)]}'
{"version": 3, "sources": ["/web/static/src/module_loader.js", "/web/static/lib/luxon/luxon.js", "/web/static/lib/owl/owl.js", "/web/static/lib/owl/odoo_module.js", "/web/static/src/env.js", "/web/static/src/session.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/cookie.js", "/web/static/src/core/browser/feature_detection.js", "/web/static/src/core/browser/router.js", "/web/static/src/core/browser/title_service.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/commands/command_category.js", "/web/static/src/core/commands/command_hook.js", "/web/static/src/core/commands/command_palette.js", "/web/static/src/core/commands/command_service.js", "/web/static/src/core/commands/default_providers.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.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", "/muk_web_dialog/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/html.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/ui.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/polyfills/clipboard.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/lib/dompurify/DOMpurify.js", "/web/static/src/model/model.js", "/web/static/src/model/record.js", "/web/static/src/model/relational_model/datapoint.js", "/web/static/src/model/relational_model/dynamic_group_list.js", "/web/static/src/model/relational_model/dynamic_list.js", "/web/static/src/model/relational_model/dynamic_record_list.js", "/web/static/src/model/relational_model/errors.js", "/web/static/src/model/relational_model/group.js", "/web/static/src/model/relational_model/record.js", "/web/static/src/model/relational_model/relational_model.js", "/web/static/src/model/relational_model/static_list.js", "/web/static/src/model/relational_model/utils.js", "/web/static/src/model/sample_server.js", "/web/static/src/search/action_hook.js", "/web/static/src/search/action_menus/action_menus.js", "/web/static/src/search/breadcrumbs/breadcrumbs.js", "/web/static/src/search/cog_menu/cog_menu.js", "/web/static/src/search/control_panel/control_panel.js", "/web/static/src/search/custom_favorite_item/custom_favorite_item.js", "/web/static/src/search/custom_group_by_item/custom_group_by_item.js", "/web/static/src/search/layout.js", "/web/static/src/search/pager_hook.js", "/web/static/src/search/properties_group_by_item/properties_group_by_item.js", "/web/static/src/search/search_arch_parser.js", "/web/static/src/search/search_bar/search_bar.js", "/web/static/src/search/search_bar/search_bar_toggler.js", "/web/static/src/search/search_bar_menu/search_bar_menu.js", "/web/static/src/search/search_model.js", "/web/static/src/search/search_panel/search_panel.js", "/web/static/src/search/utils/dates.js", "/web/static/src/search/utils/group_by.js", "/web/static/src/search/utils/misc.js", "/web/static/src/search/utils/order_by.js", "/web/static/src/search/with_search/with_search.js", "/web/static/src/views/calendar/calendar_arch_parser.js", "/web/static/src/views/calendar/calendar_common/calendar_common_popover.js", "/web/static/src/views/calendar/calendar_common/calendar_common_renderer.js", "/web/static/src/views/calendar/calendar_common/calendar_common_week_column.js", "/web/static/src/views/calendar/calendar_controller.js", "/web/static/src/views/calendar/calendar_model.js", "/web/static/src/views/calendar/calendar_renderer.js", "/web/static/src/views/calendar/calendar_view.js", "/web/static/src/views/calendar/calendar_year/calendar_year_popover.js", "/web/static/src/views/calendar/calendar_year/calendar_year_renderer.js", "/web/static/src/views/calendar/colors.js", "/web/static/src/views/calendar/filter_panel/calendar_filter_panel.js", "/web/static/src/views/calendar/hooks.js", "/web/static/src/views/calendar/mobile_filter_panel/calendar_mobile_filter_panel.js", "/web/static/src/views/calendar/quick_create/calendar_quick_create.js", "/web/static/src/views/calendar/utils.js", "/web/static/src/views/debug_items.js", "/web/static/src/views/fields/ace/ace_field.js", "/web/static/src/views/fields/attachment_image/attachment_image_field.js", "/web/static/src/views/fields/badge/badge_field.js", "/web/static/src/views/fields/badge_selection/badge_selection_field.js", "/web/static/src/views/fields/binary/binary_field.js", "/web/static/src/views/fields/boolean/boolean_field.js", "/web/static/src/views/fields/boolean_favorite/boolean_favorite_field.js", "/web/static/src/views/fields/boolean_icon/boolean_icon_field.js", "/web/static/src/views/fields/boolean_toggle/boolean_toggle_field.js", "/web/static/src/views/fields/boolean_toggle/list_boolean_toggle_field.js", "/web/static/src/views/fields/char/char_field.js", "/web/static/src/views/fields/color/color_field.js", "/web/static/src/views/fields/color_picker/color_picker_field.js", "/web/static/src/views/fields/copy_clipboard/copy_clipboard_field.js", "/web/static/src/views/fields/datetime/datetime_field.js", "/web/static/src/views/fields/datetime/list_datetime_field.js", "/web/static/src/views/fields/domain/domain_field.js", "/web/static/src/views/fields/dynamic_placeholder_hook.js", "/web/static/src/views/fields/dynamic_placeholder_popover.js", "/web/static/src/views/fields/dynamic_widget/dynamic_model_field_selector.js", "/web/static/src/views/fields/dynamic_widget/dynamic_model_field_selector_char.js", "/web/static/src/views/fields/email/email_field.js", "/web/static/src/views/fields/field.js", "/web/static/src/views/fields/field_tooltip.js", "/web/static/src/views/fields/file_handler.js", "/web/static/src/views/fields/float/float_field.js", "/web/static/src/views/fields/float_factor/float_factor_field.js", "/web/static/src/views/fields/float_time/float_time_field.js", "/web/static/src/views/fields/float_toggle/float_toggle_field.js", "/web/static/src/views/fields/formatters.js", "/web/static/src/views/fields/gauge/gauge_field.js", "/web/static/src/views/fields/google_slide_viewer/google_slide_viewer.js", "/web/static/src/views/fields/handle/handle_field.js", "/web/static/src/views/fields/html/html_field.js", "/web/static/src/views/fields/iframe_wrapper/iframe_wrapper_field.js", "/web/static/src/views/fields/image/image_field.js", "/web/static/src/views/fields/image_url/image_url_field.js", "/web/static/src/views/fields/input_field_hook.js", "/web/static/src/views/fields/integer/integer_field.js", "/web/static/src/views/fields/journal_dashboard_graph/journal_dashboard_graph_field.js", "/web/static/src/views/fields/jsonb/jsonb.js", "/web/static/src/views/fields/kanban_color_picker/kanban_color_picker_field.js", "/web/static/src/views/fields/label_selection/label_selection_field.js", "/web/static/src/views/fields/many2many_binary/many2many_binary_field.js", "/web/static/src/views/fields/many2many_checkboxes/many2many_checkboxes_field.js", "/web/static/src/views/fields/many2many_tags/kanban_many2many_tags_field.js", "/web/static/src/views/fields/many2many_tags/many2many_tags_field.js", "/web/static/src/views/fields/many2many_tags_avatar/many2many_tags_avatar_field.js", "/web/static/src/views/fields/many2one/many2one_field.js", "/web/static/src/views/fields/many2one_avatar/many2one_avatar_field.js", "/web/static/src/views/fields/many2one_barcode/many2one_barcode_field.js", "/web/static/src/views/fields/many2one_reference/many2one_reference_field.js", "/web/static/src/views/fields/many2one_reference_integer/many2one_reference_integer_field.js", "/web/static/src/views/fields/monetary/monetary_field.js", "/web/static/src/views/fields/numpad_decimal_hook.js", "/web/static/src/views/fields/parsers.js", "/web/static/src/views/fields/pdf_viewer/pdf_viewer_field.js", "/web/static/src/views/fields/percent_pie/percent_pie_field.js", "/web/static/src/views/fields/percentage/percentage_field.js", "/web/static/src/views/fields/phone/phone_field.js", "/web/static/src/views/fields/priority/priority_field.js", "/web/static/src/views/fields/progress_bar/kanban_progress_bar_field.js", "/web/static/src/views/fields/progress_bar/progress_bar_field.js", "/web/static/src/views/fields/properties/card_properties_field.js", "/web/static/src/views/fields/properties/properties_field.js", "/web/static/src/views/fields/properties/property_definition.js", "/web/static/src/views/fields/properties/property_definition_selection.js", "/web/static/src/views/fields/properties/property_tags.js", "/web/static/src/views/fields/properties/property_value.js", "/web/static/src/views/fields/radio/radio_field.js", "/web/static/src/views/fields/reference/reference_field.js", "/web/static/src/views/fields/relational_utils.js", "/web/static/src/views/fields/remaining_days/remaining_days_field.js", "/web/static/src/views/fields/selection/filterable_selection_field.js", "/web/static/src/views/fields/selection/selection_field.js", "/web/static/src/views/fields/signature/signature_field.js", "/web/static/src/views/fields/standard_field_props.js", "/web/static/src/views/fields/stat_info/stat_info_field.js", "/web/static/src/views/fields/state_selection/state_selection_field.js", "/web/static/src/views/fields/statusbar/statusbar_field.js", "/web/static/src/views/fields/text/text_field.js", "/web/static/src/views/fields/timezone_mismatch/timezone_mismatch_field.js", "/web/static/src/views/fields/translation_button.js", "/web/static/src/views/fields/translation_dialog.js", "/web/static/src/views/fields/url/url_field.js", "/web/static/src/views/fields/x2many/list_x2many_field.js", "/web/static/src/views/fields/x2many/x2many_field.js", "/web/static/src/views/form/button_box/button_box.js", "/web/static/src/views/form/form_arch_parser.js", "/web/static/src/views/form/form_cog_menu/form_cog_menu.js", "/web/static/src/views/form/form_compiler.js", "/web/static/src/views/form/form_controller.js", "/knowledge/static/src/web/form_controller_patch.js", "/web/static/src/views/form/form_error_dialog/form_error_dialog.js", "/web/static/src/views/form/form_group/form_group.js", "/web/static/src/views/form/form_label.js", "/web/static/src/views/form/form_renderer.js", "/web/static/src/views/form/form_status_indicator/form_status_indicator.js", "/web/static/src/views/form/form_view.js", "/web/static/src/views/form/setting/setting.js", "/web/static/src/views/form/status_bar_buttons/status_bar_buttons.js", "/web/static/src/views/form/status_bar_dropdown_items/status_bar_dropdown_items.js", "/web/static/src/views/kanban/kanban_arch_parser.js", "/web/static/src/views/kanban/kanban_color_picker_legacy.js", "/web/static/src/views/kanban/kanban_column_examples_dialog.js", "/web/static/src/views/kanban/kanban_column_quick_create.js", "/web/static/src/views/kanban/kanban_compiler.js", "/web/static/src/views/kanban/kanban_controller.js", "/web/static/src/views/kanban/kanban_cover_image_dialog.js", "/web/static/src/views/kanban/kanban_dropdown_menu_wrapper.js", "/web/static/src/views/kanban/kanban_header.js", "/web/static/src/views/kanban/kanban_record.js", "/web/static/src/views/kanban/kanban_record_quick_create.js", "/web/static/src/views/kanban/kanban_renderer.js", "/web/static/src/views/kanban/kanban_view.js", "/web/static/src/views/kanban/progress_bar_hook.js", "/web/static/src/views/list/column_width_hook.js", "/web/static/src/views/list/export_all/export_all.js", "/web/static/src/views/list/list_arch_parser.js", "/web/static/src/views/list/list_cog_menu.js", "/web/static/src/views/list/list_confirmation_dialog.js", "/web/static/src/views/list/list_controller.js", "/web/static/src/views/list/list_renderer.js", "/web/static/src/views/list/list_view.js", "/web/static/src/views/standard_view_props.js", "/web/static/src/views/utils.js", "/web/static/src/views/view.js", "/web/static/src/views/view_button/multi_record_view_button.js", "/web/static/src/views/view_button/view_button.js", "/web/static/src/views/view_button/view_button_hook.js", "/web/static/src/views/view_compiler.js", "/web/static/src/views/view_components/animated_number.js", "/web/static/src/views/view_components/column_progress.js", "/web/static/src/views/view_components/report_view_measures.js", "/web/static/src/views/view_components/view_scale_selector.js", "/web/static/src/views/view_dialogs/export_data_dialog.js", "/web/static/src/views/view_dialogs/form_view_dialog.js", "/web/static/src/views/view_dialogs/select_create_dialog.js", "/web/static/src/views/view_hook.js", "/web/static/src/views/view_service.js", "/web/static/src/views/widgets/attach_document/attach_document.js", "/web/static/src/views/widgets/documentation_link/documentation_link.js", "/web/static/src/views/widgets/notification_alert/notification_alert.js", "/web/static/src/views/widgets/ribbon/ribbon.js", "/web/static/src/views/widgets/signature/signature.js", "/web/static/src/views/widgets/standard_widget_props.js", "/web/static/src/views/widgets/week_days/week_days.js", "/web/static/src/views/widgets/widget.js", "/web/static/src/webclient/actions/action_container.js", "/web/static/src/webclient/actions/action_dialog.js", "/web/static/src/webclient/actions/action_install_kiosk_pwa.js", "/web/static/src/webclient/actions/action_service.js", "/web/static/src/webclient/actions/client_actions.js", "/web/static/src/webclient/actions/debug_items.js", "/web/static/src/webclient/burger_menu/burger_menu.js", "/web/static/src/webclient/burger_menu/burger_user_menu/burger_user_menu.js", "/web/static/src/webclient/burger_menu/mobile_switch_company_menu/mobile_switch_company_menu.js", "/web/static/src/webclient/clickbot/clickbot_loader.js", "/web/static/src/webclient/company_service.js", "/web/static/src/webclient/currency_service.js", "/web/static/src/webclient/debug/debug_items.js", "/web/static/src/webclient/debug/profiling/profiling_item.js", "/web/static/src/webclient/debug/profiling/profiling_qweb.js", "/web/static/src/webclient/debug/profiling/profiling_service.js", "/web/static/src/webclient/debug/profiling/profiling_systray_item.js", "/web/static/src/webclient/loading_indicator/loading_indicator.js", "/web/static/src/webclient/menus/menu_helpers.js", "/web/static/src/webclient/menus/menu_providers.js", "/web/static/src/webclient/menus/menu_service.js", "/web/static/src/webclient/navbar/navbar.js", "/web/static/src/webclient/settings_form_view/fields/settings_binary_field/settings_binary_field.js", "/web/static/src/webclient/settings_form_view/fields/upgrade_boolean_field.js", "/web/static/src/webclient/settings_form_view/fields/upgrade_dialog.js", "/web/static/src/webclient/settings_form_view/highlight_text/form_label_highlight_text.js", "/web/static/src/webclient/settings_form_view/highlight_text/highlight_text.js", "/web/static/src/webclient/settings_form_view/highlight_text/settings_radio_field.js", "/web/static/src/webclient/settings_form_view/settings/searchable_setting.js", "/web/static/src/webclient/settings_form_view/settings/setting_header.js", "/web/static/src/webclient/settings_form_view/settings/settings_app.js", "/web/static/src/webclient/settings_form_view/settings/settings_block.js", "/web/static/src/webclient/settings_form_view/settings/settings_page.js", "/web/static/src/webclient/settings_form_view/settings_confirmation_dialog.js", "/web/static/src/webclient/settings_form_view/settings_form_compiler.js", "/web/static/src/webclient/settings_form_view/settings_form_controller.js", "/web/static/src/webclient/settings_form_view/settings_form_renderer.js", "/web/static/src/webclient/settings_form_view/settings_form_view.js", "/web/static/src/webclient/settings_form_view/widgets/demo_data_service.js", "/web/static/src/webclient/settings_form_view/widgets/mobile_apps_funnel.js", "/web/static/src/webclient/settings_form_view/widgets/res_config_dev_tool.js", "/web/static/src/webclient/settings_form_view/widgets/res_config_edition.js", "/web/static/src/webclient/settings_form_view/widgets/res_config_invite_users.js", "/web/static/src/webclient/settings_form_view/widgets/user_invite_service.js", "/web/static/src/webclient/share_target/share_target_service.js", "/web/static/src/webclient/switch_company_menu/switch_company_item.js", "/web/static/src/webclient/switch_company_menu/switch_company_menu.js", "/web/static/src/webclient/user_menu/user_menu.js", "/web/static/src/webclient/user_menu/user_menu_items.js", "/web/static/src/webclient/webclient.js", "/muk_web_appsbar/static/src/webclient/appsbar/appsbar.js", "/muk_web_appsbar/static/src/webclient/menus/app_menu_service.js", "/muk_web_appsbar/static/src/webclient/webclient.js", "/web/static/src/webclient/actions/reports/report_action.js", "/web/static/src/webclient/actions/reports/report_hook.js", "/web/static/src/webclient/actions/reports/utils.js", "/web/static/src/libs/pdfjs.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/outdated_page_watcher_service.js", "/bus/static/src/simple_notification_service.js", "/bus/static/src/services/assets_watchdog_service.js", "/bus/static/src/services/bus_monitoring_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_recorder/tour_recorder_state.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_tour/static/src/views/tour_list.js", "/web_tour/static/src/widgets/tour_start.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/components/history_dialog/history_dialog.js", "/html_editor/static/src/core/base_container_plugin.js", "/html_editor/static/src/core/clipboard_plugin.js", "/html_editor/static/src/core/comment_plugin.js", "/html_editor/static/src/core/delete_plugin.js", "/html_editor/static/src/core/dialog_plugin.js", "/html_editor/static/src/core/dom_plugin.js", "/html_editor/static/src/core/editor_version_plugin.js", "/html_editor/static/src/core/format_plugin.js", "/html_editor/static/src/core/history_plugin.js", "/html_editor/static/src/core/input_plugin.js", "/html_editor/static/src/core/line_break_plugin.js", "/html_editor/static/src/core/no_inline_root_plugin.js", "/html_editor/static/src/core/overlay.js", "/html_editor/static/src/core/overlay_plugin.js", "/html_editor/static/src/core/protected_node_plugin.js", "/html_editor/static/src/core/sanitize_plugin.js", "/html_editor/static/src/core/selection_plugin.js", "/html_editor/static/src/core/shortcut_plugin.js", "/html_editor/static/src/core/split_plugin.js", "/html_editor/static/src/core/user_command_plugin.js", "/html_editor/static/src/editor.js", "/html_editor/static/src/fields/html_field.js", "/html_editor/static/src/fields/html_viewer.js", "/html_editor/static/src/html_migrations/html_migrations_utils.js", "/html_editor/static/src/html_migrations/html_upgrade_manager.js", "/html_editor/static/src/html_migrations/manifest.js", "/html_editor/static/src/html_migrations/migration-1.1.js", "/html_editor/static/src/local_overlay_container.js", "/html_editor/static/src/main/align_plugin.js", "/html_editor/static/src/main/banner_plugin.js", "/html_editor/static/src/main/chatgpt/chatgpt_alternatives_dialog.js", "/html_editor/static/src/main/chatgpt/chatgpt_dialog.js", "/html_editor/static/src/main/chatgpt/chatgpt_plugin.js", "/html_editor/static/src/main/chatgpt/chatgpt_prompt_dialog.js", "/html_editor/static/src/main/chatgpt/chatgpt_translate_dialog.js", "/html_editor/static/src/main/chatgpt/language_selector.js", "/html_editor/static/src/main/column_plugin.js", "/html_editor/static/src/main/emoji_plugin.js", "/html_editor/static/src/main/feff_plugin.js", "/html_editor/static/src/main/font/color_plugin.js", "/html_editor/static/src/main/font/color_selector.js", "/html_editor/static/src/main/font/font_plugin.js", "/html_editor/static/src/main/font/font_selector.js", "/html_editor/static/src/main/font/font_size_selector.js", "/html_editor/static/src/main/font/gradient_picker.js", "/html_editor/static/src/main/hint_plugin.js", "/html_editor/static/src/main/inline_code.js", "/html_editor/static/src/main/link/command_category.js", "/html_editor/static/src/main/link/link_paste_plugin.js", "/html_editor/static/src/main/link/link_plugin.js", "/html_editor/static/src/main/link/link_popover.js", "/html_editor/static/src/main/link/link_selection_odoo_plugin.js", "/html_editor/static/src/main/link/link_selection_plugin.js", "/html_editor/static/src/main/link/utils.js", "/html_editor/static/src/main/list/list_plugin.js", "/html_editor/static/src/main/list/utils.js", "/html_editor/static/src/main/local_overlay_plugin.js", "/html_editor/static/src/main/media/file_plugin.js", "/html_editor/static/src/main/media/icon_plugin.js", "/html_editor/static/src/main/media/image_crop.js", "/html_editor/static/src/main/media/image_crop_plugin.js", "/html_editor/static/src/main/media/image_description.js", "/html_editor/static/src/main/media/image_padding.js", "/html_editor/static/src/main/media/image_plugin.js", "/html_editor/static/src/main/media/image_transform_button.js", "/html_editor/static/src/main/media/image_transformation.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/main/media/media_plugin.js", "/html_editor/static/src/main/movenode_plugin.js", "/html_editor/static/src/main/position_plugin.js", "/html_editor/static/src/main/power_buttons_plugin.js", "/html_editor/static/src/main/powerbox/powerbox.js", "/html_editor/static/src/main/powerbox/powerbox_plugin.js", "/html_editor/static/src/main/powerbox/search_powerbox_plugin.js", "/html_editor/static/src/main/signature_plugin.js", "/html_editor/static/src/main/star_plugin.js", "/html_editor/static/src/main/table/table_menu.js", "/html_editor/static/src/main/table/table_picker.js", "/html_editor/static/src/main/table/table_plugin.js", "/html_editor/static/src/main/table/table_resize_plugin.js", "/html_editor/static/src/main/table/table_ui_plugin.js", "/html_editor/static/src/main/tabulation_plugin.js", "/html_editor/static/src/main/text_direction_plugin.js", "/html_editor/static/src/main/toolbar/mobile_toolbar.js", "/html_editor/static/src/main/toolbar/toolbar.js", "/html_editor/static/src/main/toolbar/toolbar_plugin.js", "/html_editor/static/src/main/youtube_plugin.js", "/html_editor/static/src/others/collaboration/PeerToPeer.js", "/html_editor/static/src/others/collaboration/collaboration_odoo_plugin.js", "/html_editor/static/src/others/collaboration/collaboration_plugin.js", "/html_editor/static/src/others/collaboration/collaboration_selection_avatar_plugin.js", "/html_editor/static/src/others/collaboration/collaboration_selection_plugin.js", "/html_editor/static/src/others/custom_media_dialog.js", "/html_editor/static/src/others/dynamic_placeholder_plugin.js", "/html_editor/static/src/others/embedded_component_plugin.js", "/html_editor/static/src/others/embedded_component_utils.js", "/html_editor/static/src/others/embedded_components/backend/file/file.js", "/html_editor/static/src/others/embedded_components/core/embedded_component_toolbar/embedded_component_toolbar.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", "/html_editor/static/src/others/embedded_components/embedding_sets.js", "/html_editor/static/src/others/embedded_components/plugins/embedded_file_plugin/embedded_file_documents_selector.js", "/html_editor/static/src/others/embedded_components/plugins/embedded_file_plugin/embedded_file_plugin.js", "/html_editor/static/src/others/embedded_components/plugins/file_plugin/file_plugin.js", "/html_editor/static/src/others/embedded_components/plugins/table_of_content_plugin/table_of_content_plugin.js", "/html_editor/static/src/others/embedded_components/plugins/video_plugin/video_plugin.js", "/html_editor/static/src/others/embedded_components/plugins/video_plugin/video_selector_dialog/video_selector_dialog.js", "/html_editor/static/src/others/qweb_picker.js", "/html_editor/static/src/others/qweb_plugin.js", "/html_editor/static/src/others/x2many_image_field.js", "/html_editor/static/src/others/x2many_media_viewer.js", "/html_editor/static/src/plugin.js", "/html_editor/static/src/plugin_sets.js", "/html_editor/static/src/position_hook.js", "/html_editor/static/src/services/upload_local_files_service.js", "/html_editor/static/src/utils/base_container.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", "/html_editor/static/src/wysiwyg.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", "/mail/static/lib/selfie_segmentation/selfie_segmentation.js", "/mail/static/src/js/emojis_mixin.js", "/mail/static/src/js/onchange_on_keydown.js", "/mail/static/src/js/tools/debug_manager.js", "/mail/static/src/js/tours/discuss_channel_tour.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_attachment_dropzone.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/res_groups_model.js", "/mail/static/src/core/common/search_message_input.js", "/mail/static/src/core/common/search_message_result.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/core/public_web/chat_hub_patch.js", "/mail/static/src/core/public_web/discuss.js", "/mail/static/src/core/public_web/discuss_app_category_model.js", "/mail/static/src/core/public_web/discuss_app_model.js", "/mail/static/src/core/public_web/discuss_client_action.js", "/mail/static/src/core/public_web/discuss_sidebar.js", "/mail/static/src/core/public_web/messaging_menu.js", "/mail/static/src/core/public_web/notification_item.js", "/mail/static/src/core/public_web/out_of_focus_service_patch.js", "/mail/static/src/core/public_web/store_service_patch.js", "/mail/static/src/core/public_web/thread_model_patch.js", "/mail/static/src/core/web_portal/message_patch.js", "/mail/static/src/core/web/activity.js", "/mail/static/src/core/web/activity_button.js", "/mail/static/src/core/web/activity_list_popover.js", "/mail/static/src/core/web/activity_list_popover_item.js", "/mail/static/src/core/web/activity_mail_template.js", "/mail/static/src/core/web/activity_markasdone_popover.js", "/mail/static/src/core/web/activity_menu.js", "/mail/static/src/core/web/activity_model.js", "/mail/static/src/core/web/base_recipients_list.js", "/mail/static/src/core/web/chat_window_model_patch.js", "/mail/static/src/core/web/command_category.js", "/mail/static/src/core/web/composer_patch.js", "/mail/static/src/core/web/dialog_patch.js", "/mail/static/src/core/web/discuss_patch.js", "/mail/static/src/core/web/discuss_sidebar_mailboxes.js", "/mail/static/src/core/web/discuss_sidebar_patch.js", "/mail/static/src/core/web/follower_list.js", "/mail/static/src/core/web/follower_subtype_dialog.js", "/mail/static/src/core/web/mail_column_progress.js", "/mail/static/src/core/web/mail_composer_attachment_list.js", "/mail/static/src/core/web/mail_composer_attachment_selector.js", "/mail/static/src/core/web/mail_composer_chatgpt.js", "/mail/static/src/core/web/mail_composer_recipient_list.js", "/mail/static/src/core/web/mail_composer_template_selector.js", "/mail/static/src/core/web/mail_core_common_service_patch.js", "/mail/static/src/core/web/mail_core_web_service.js", "/mail/static/src/core/web/mention_list.js", "/mail/static/src/core/web/message_patch.js", "/mail/static/src/core/web/messaging_menu_patch.js", "/mail/static/src/core/web/messaging_menu_quick_search.js", "/mail/static/src/core/web/open_chat_hook.js", "/mail/static/src/core/web/recipient_list.js", "/mail/static/src/core/web/store_service_patch.js", "/mail/static/src/core/web/suggested_recipient.js", "/mail/static/src/core/web/suggested_recipient_list.js", "/mail/static/src/core/web/thread_actions.js", "/mail/static/src/core/web/thread_model_patch.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/html.js", "/mail/static/src/utils/common/misc.js", "/mail/static/src/chatter/web_portal/chatter.js", "/muk_web_chatter/static/src/chatter/chatter.js", "/mail/static/src/chatter/web_portal/composer_patch.js", "/mail/static/src/chatter/web_portal/thread_model_patch.js", "/mail/static/src/chatter/web/chatter_patch.js", "/mail/static/src/chatter/web/form_arch_parser.js", "/mail/static/src/chatter/web/form_compiler.js", "/muk_web_chatter/static/src/views/form/form_compiler.js", "/mail/static/src/chatter/web/form_controller.js", "/mail/static/src/chatter/web/form_renderer.js", "/mail/static/src/chatter/web/mail_composer_form.js", "/mail/static/src/chatter/web/mail_composer_schedule_dialog.js", "/mail/static/src/chatter/web/mail_composer_send_dropdown.js", "/mail/static/src/chatter/web/scheduled_message.js", "/mail/static/src/chatter/web/scheduled_message_model.js", "/mail/static/src/chatter/web/thread_model_patch.js", "/mail/static/src/views/web/fields/activity_exception/activity_exception.js", "/mail/static/src/views/web/fields/assign_user_command_hook.js", "/mail/static/src/views/web/fields/avatar/avatar.js", "/mail/static/src/views/web/fields/emojis_char_field/emojis_char_field.js", "/mail/static/src/views/web/fields/emojis_field_common/emojis_field_common.js", "/mail/static/src/views/web/fields/emojis_text_field/emojis_text_field.js", "/mail/static/src/views/web/fields/html_composer_message_field/html_composer_message_field.js", "/mail/static/src/views/web/fields/html_composer_message_field/mention_plugin.js", "/mail/static/src/views/web/fields/html_mail_field/convert_inline.js", "/mail/static/src/views/web/fields/html_mail_field/html_mail_field.js", "/mail/static/src/views/web/fields/kanban_activity/kanban_activity.js", "/mail/static/src/views/web/fields/list_activity/list_activity.js", "/mail/static/src/views/web/fields/many2many_avatar_user_field/many2many_avatar_user_field.js", "/mail/static/src/views/web/fields/many2many_tags_email/many2many_tags_email.js", "/mail/static/src/views/web/fields/many2one_avatar_user_field/many2one_avatar_user_field.js", "/mail/static/src/views/web/fields/properties_field/property_value.js", "/mail/static/src/views/web/list/archive_disabled_list_controller.js", "/mail/static/src/views/web/list/archive_disabled_list_view.js", "/mail/static/src/views/web/list_renderer.js", "/mail/static/src/views/web/model/sample_server_patch.js", "/mail/static/src/webclient/web/webclient.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/core/public_web/bus_connection_alert.js", "/mail/static/src/discuss/core/public_web/discuss_client_action_patch.js", "/mail/static/src/discuss/core/public_web/discuss_core_public_web_service.js", "/mail/static/src/discuss/core/public_web/discuss_sidebar_categories.js", "/mail/static/src/discuss/core/public_web/message_actions.js", "/mail/static/src/discuss/core/public_web/message_model_patch.js", "/mail/static/src/discuss/core/public_web/message_patch.js", "/mail/static/src/discuss/core/public_web/store_service_patch.js", "/mail/static/src/discuss/core/public_web/sub_channel_list.js", "/mail/static/src/discuss/core/public_web/thread_actions.js", "/mail/static/src/discuss/core/public_web/thread_model_patch.js", "/mail/static/src/discuss/core/public_web/thread_patch.js", "/mail/static/src/discuss/core/web/channel_member_list_patch.js", "/mail/static/src/discuss/core/web/channel_selector.js", "/mail/static/src/discuss/core/web/chat_window_patch.js", "/mail/static/src/discuss/core/web/command_palette.js", "/mail/static/src/discuss/core/web/discuss_core_common_service_patch.js", "/mail/static/src/discuss/core/web/discuss_core_web_service.js", "/mail/static/src/discuss/core/web/discuss_sidebar_categories_patch.js", "/mail/static/src/discuss/core/web/messaging_menu_patch.js", "/mail/static/src/discuss/core/web/store_service_patch.js", "/mail/static/src/discuss/core/web/thread_actions.js", "/mail/static/src/discuss/core/web/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/gif_picker/common/composer_patch.js", "/mail/static/src/discuss/gif_picker/common/gif_picker.js", "/mail/static/src/discuss/gif_picker/common/picker_content_patch.js", "/mail/static/src/discuss/gif_picker/common/picker_patch.js", "/mail/static/src/discuss/gif_picker/common/store_service_patch.js", "/mail/static/src/discuss/message_pin/common/message_actions.js", "/mail/static/src/discuss/message_pin/common/message_model_patch.js", "/mail/static/src/discuss/message_pin/common/message_patch.js", "/mail/static/src/discuss/message_pin/common/pinned_messages_panel.js", "/mail/static/src/discuss/message_pin/common/thread_actions.js", "/mail/static/src/discuss/message_pin/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/discuss/voice_message/common/attachment_list_patch.js", "/mail/static/src/discuss/voice_message/common/attachment_model_patch.js", "/mail/static/src/discuss/voice_message/common/attachment_uploader_hook_patch.js", "/mail/static/src/discuss/voice_message/common/composer_model_patch.js", "/mail/static/src/discuss/voice_message/common/composer_patch.js", "/mail/static/src/discuss/voice_message/common/mp3_encoder.js", "/mail/static/src/discuss/voice_message/common/voice_message_service.js", "/mail/static/src/discuss/voice_message/common/voice_player.js", "/mail/static/src/discuss/voice_message/common/voice_recorder.js", "/mail/static/src/discuss/call/public_web/discuss_sidebar_call_indicator.js", "/mail/static/src/discuss/call/public_web/discuss_sidebar_call_participants.js", "/mail/static/src/discuss/call/public_web/discuss_sidebar_categories_patch.js", "/mail/static/src/discuss/public_web/discuss_patch.js", "/mail/static/src/discuss/call/web/chat_window_patch.js", "/mail/static/src/discuss/call/web/discuss_patch.js", "/mail/static/src/discuss/call/web/messaging_menu_patch.js", "/mail/static/src/discuss/web/avatar_card/avatar_card_popover.js", "/mail/static/src/discuss/web/discuss_core_common_service_patch.js", "/mail/static/src/views/fields/statusbar_duration/statusbar_duration_field.js", "/onboarding/static/src/views/form/onboarding_step_form_controller.js", "/product/static/src/js/pricelist_report/product_pricelist_report.js", "/product/static/src/js/product_attribute_value_list.js", "/product/static/src/js/product_document_kanban/product_document_kanban_controller.js", "/product/static/src/js/product_document_kanban/product_document_kanban_record.js", "/product/static/src/js/product_document_kanban/product_document_kanban_renderer.js", "/product/static/src/js/product_document_kanban/product_document_kanban_view.js", "/product/static/src/js/product_document_kanban/upload_button/upload_button.js", "/product/static/src/product_catalog/kanban_controller.js", "/product/static/src/product_catalog/kanban_model.js", "/product/static/src/product_catalog/kanban_record.js", "/product/static/src/product_catalog/kanban_renderer.js", "/product/static/src/product_catalog/kanban_view.js", "/product/static/src/product_catalog/order_line/order_line.js", "/product/static/src/product_catalog/search/search_panel.js", "/analytic/static/src/components/analytic_distribution/analytic_distribution.js", "/analytic/static/src/services/batched_orm_service.js", "/web_editor/static/lib/vkbeautify/vkbeautify.0.99.00.beta.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/editor/odoo-editor/src/utils/utils.js", "/web_editor/static/src/js/wysiwyg/fonts.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/backend/QWebPlugin.js", "/web_editor/static/src/js/backend/convert_inline.js", "/web_editor/static/src/js/backend/html_field.js", "/portal/static/src/views/fields/portal_wizard_user_one2many.js", "/portal/static/src/views/list/portal_wizard_user_list_controller.js", "/resource/static/src/section_list_renderer.js", "/resource/static/src/section_one2many_field.js", "/resource/static/src/views/form_with_html_expander/form_controller_with_html_expander.js", "/resource/static/src/views/form_with_html_expander/form_renderer_with_html_expander.js", "/resource/static/src/views/form_with_html_expander/form_view_with_html_expander.js", "/account/static/src/components/account_batch_sending_summary/account_batch_sending_summary.js", "/account/static/src/components/account_file_uploader/account_file_uploader.js", "/account/static/src/components/account_many2one_barcode/account_many2one_barcode.js", "/account/static/src/components/account_merge_wizard_line_one2many/account_merge_wizard_line_one2many.js", "/account/static/src/components/account_move_form/account_move_form.js", "/account/static/src/components/account_payment_field/account_payment_field.js", "/account/static/src/components/account_payment_register_html/account_payment_register_html.js", "/account/static/src/components/account_payment_term_form/payment_term_line_ids.js", "/account/static/src/components/account_resequence/account_resequence_field.js", "/account/static/src/components/account_statusbar_secured/account_move_statusbar_secured.js", "/account/static/src/components/account_type_selection/account_type_selection.js", "/account/static/src/components/actionable_errors/actionable_errors.js", "/account/static/src/components/auto_save_res_partner_bank/auto_save_res_partner_bank.js", "/account/static/src/components/autosave_many2many_tags/autosave_many2many_tags.js", "/account/static/src/components/bill_guide/bill_guide.js", "/account/static/src/components/char_with_placeholder_field/char_with_placeholder_field.js", "/account/static/src/components/document_file_uploader/document_file_uploader.js", "/account/static/src/components/document_state/document_state_field.js", "/account/static/src/components/dynamic_selection/dynamic_selection.js", "/account/static/src/components/grouped_view_widget/grouped_view_widget.js", "/account/static/src/components/journal_dashboard_activity/journal_dashboard_activity.js", "/account/static/src/components/json_checkboxes/json_checkboxes.js", "/account/static/src/components/mail_attachments/mail_attachments.js", "/account/static/src/components/many2many_tax_tags/many2many_tax_tags.js", "/account/static/src/components/onboarding/onboarding.js", "/account/static/src/components/open_move_line_move_widget/open_move_line_move_widget.js", "/account/static/src/components/open_move_widget/open_move_widget.js", "/account/static/src/components/product_catalog/account_move_line.js", "/account/static/src/components/product_catalog/kanban_controller.js", "/account/static/src/components/product_catalog/kanban_record.js", "/account/static/src/components/product_label_section_and_note_field/product_label_section_and_note_field.js", "/account/static/src/components/section_and_note_fields_backend/section_and_note_fields_backend.js", "/account/static/src/components/tax_autocomplete/tax_autocomplete.js", "/account/static/src/components/tax_totals/tax_totals.js", "/account/static/src/components/tests_shared_js_python/tests_shared_js_python.js", "/account/static/src/components/upload_drop_zone/upload_drop_zone.js", "/account/static/src/components/x2many_buttons/x2many_buttons.js", "/account/static/src/services/account_move_service.js", "/account/static/src/services/account_notification_service.js", "/account/static/src/views/account_dashboard_kanban/account_dashboard_kanban_record.js", "/account/static/src/views/account_dashboard_kanban/account_dashboard_kanban_renderer.js", "/account/static/src/views/account_dashboard_kanban/account_dashboard_kanban_view.js", "/account/static/src/views/account_move_kanban/account_move_kanban_controller.js", "/account/static/src/views/account_move_kanban/account_move_kanban_view.js", "/account/static/src/views/account_move_list/account_move_list_controller.js", "/account/static/src/views/account_move_list/account_move_list_renderer.js", "/account/static/src/views/account_move_list/account_move_list_view.js", "/account/static/src/views/account_x2many_list_controller.js", "/account/static/src/views/file_upload_kanban/file_upload_kanban_controller.js", "/account/static/src/views/file_upload_kanban/file_upload_kanban_renderer.js", "/account/static/src/views/file_upload_kanban/file_upload_kanban_view.js", "/account/static/src/views/file_upload_list/file_upload_list_controller.js", "/account/static/src/views/file_upload_list/file_upload_list_renderer.js", "/account/static/src/views/file_upload_list/file_upload_list_view.js", "/account/static/src/views/upload_file_from_data_hook.js", "/account/static/src/js/tours/account.js", "/account/static/src/js/search/search_bar/search_bar.js", "/account/static/src/helpers/account_tax.js", "/account/static/src/core/utils/product_and_label_autoresize.js", "/payment/static/src/js/payment_wizard_copy_clipboard_field.js", "/utm/static/src/js/utm_campaign_kanban_examples.js", "/sale/static/src/js/badge_extra_price/badge_extra_price.js", "/sale/static/src/js/sale_action_helper/sale_action_helper.js", "/sale/static/src/js/sale_action_helper/sale_action_helper_dialog.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_order_line_field/sale_order_line_field.js", "/sale/static/src/js/sale_progressbar_field.js", "/sale/static/src/js/tours/sale.js", "/sale/static/src/js/sale_product_field.js", "/sale/static/src/js/sale_utils.js", "/sale/static/src/views/sale_onboarding_kanban/sale_onboarding_kanban_renderer.js", "/sale/static/src/views/sale_onboarding_kanban/sale_onboarding_kanban_view.js", "/sale/static/src/views/sale_onboarding_list/sale_onboarding_list_renderer.js", "/sale/static/src/views/sale_onboarding_list/sale_onboarding_list_view.js", "/calendar/static/src/activity/activity_list_popover_item_patch.js", "/calendar/static/src/activity/activity_menu_patch.js", "/calendar/static/src/activity/activity_model_patch.js", "/calendar/static/src/activity/activity_patch.js", "/calendar/static/src/components/calendar_provider_config/calendar_connect_provider.js", "/calendar/static/src/core/web/store_service_patch.js", "/calendar/static/src/js/services/calendar_notification_service.js", "/calendar/static/src/views/ask_recurrence_update_policy_dialog.js", "/calendar/static/src/views/ask_recurrence_update_policy_hook.js", "/calendar/static/src/views/attendee_calendar/attendee_calendar_controller.js", "/calendar/static/src/views/attendee_calendar/attendee_calendar_model.js", "/calendar/static/src/views/attendee_calendar/attendee_calendar_renderer.js", "/calendar/static/src/views/attendee_calendar/attendee_calendar_view.js", "/calendar/static/src/views/attendee_calendar/common/attendee_calendar_common_popover.js", "/calendar/static/src/views/attendee_calendar/common/attendee_calendar_common_renderer.js", "/calendar/static/src/views/attendee_calendar/year/attendee_calendar_year_popover.js", "/calendar/static/src/views/attendee_calendar/year/attendee_calendar_year_renderer.js", "/calendar/static/src/views/calendar_form/calendar_form_controller.js", "/calendar/static/src/views/calendar_form/calendar_form_view.js", "/calendar/static/src/views/calendar_form/calendar_quick_create.js", "/calendar/static/src/views/fields/attendee_tags_list.js", "/calendar/static/src/views/fields/many2many_attendee.js", "/calendar/static/src/views/fields/many2many_attendee_expandable.js", "/calendar/static/src/views/list_view/calendar_list_view.js", "/calendar/static/src/views/widgets/calendar_week_days.js", "/crm/static/src/activity_menu_patch.js", "/crm/static/src/js/tours/crm.js", "/crm/static/src/views/check_rainbowman_message.js", "/crm/static/src/views/crm_form/crm_form.js", "/crm/static/src/views/crm_kanban/crm_column_progress.js", "/crm/static/src/views/crm_kanban/crm_kanban_arch_parser.js", "/crm/static/src/views/crm_kanban/crm_kanban_model.js", "/crm/static/src/views/crm_kanban/crm_kanban_renderer.js", "/crm/static/src/views/crm_kanban/crm_kanban_view.js", "/crm/static/src/views/fill_temporal_service.js", "/crm/static/src/views/forecast_kanban/forecast_kanban_column_quick_create.js", "/crm/static/src/views/forecast_kanban/forecast_kanban_controller.js", "/crm/static/src/views/forecast_kanban/forecast_kanban_model.js", "/crm/static/src/views/forecast_kanban/forecast_kanban_renderer.js", "/crm/static/src/views/forecast_kanban/forecast_kanban_view.js", "/crm/static/src/views/forecast_list/forecast_list_view.js", "/crm/static/src/views/forecast_search_model.js", "/website/static/src/components/resource_editor/resource_editor.js", "/website/static/src/components/resource_editor/resource_editor_warning.js", "/website/static/src/components/resource_editor/utils.js", "/website/static/src/components/edit_head_body_dialog/edit_head_body_dialog.js", "/website/static/src/components/dialog/add_page_dialog.js", "/website/static/src/components/dialog/dialog.js", "/website/static/src/components/dialog/edit_menu.js", "/website/static/src/components/dialog/page_properties.js", "/website/static/src/components/dialog/seo.js", "/website/static/src/components/editor/editor.js", "/website/static/src/components/navbar/navbar.js", "/website/static/src/components/burger_menu/burger_menu.js", "/website/static/src/components/switch/switch.js", "/website/static/src/components/wysiwyg_adapter/page_options.js", "/website/static/src/components/translator/translator.js", "/website/static/src/js/new_content_form.js", "/website/static/src/services/website_custom_menus.js", "/website/static/src/js/tours/homepage.js", "/website/static/src/systray_items/edit_in_backend.js", "/website/static/src/systray_items/edit_website.js", "/website/static/src/systray_items/mobile_preview.js", "/website/static/src/systray_items/new_content.js", "/website/static/src/systray_items/publish.js", "/website/static/src/systray_items/website_switcher.js", "/website/static/src/js/backend/view_hierarchy/hierarchy_navbar.js", "/website/static/src/js/backend/view_hierarchy/view_hierarchy.js", "/website_helpdesk/static/src/js/website_helpdesk_edit_menu.js", "/website_enterprise/static/src/js/systray_items/new_content.js", "/website_enterprise/static/src/services/color_scheme_service_patch.js", "/website/static/src/js/tours/tour_utils.js", "/website/static/src/js/text_processing.js", "/website/static/src/client_actions/configurator/configurator.js", "/website/static/src/client_actions/open_custom_menu/open_custom_menu.js", "/website/static/src/client_actions/website_dashboard/website_dashboard.js", "/website/static/src/client_actions/website_preview/website_preview.js", "/website/static/src/components/fields/fields.js", "/website/static/src/components/fields/publish_button.js", "/website/static/src/components/fields/redirect_field.js", "/website/static/src/components/fields/widget_iframe.js", "/website/static/src/components/fullscreen_indication/fullscreen_indication.js", "/website/static/src/components/website_loader/website_loader.js", "/website/static/src/components/views/page_kanban.js", "/website/static/src/components/views/page_list.js", "/website/static/src/components/views/page_search_model.js", "/website/static/src/components/views/page_views_mixin.js", "/website/static/src/components/views/theme_preview_form.js", "/website/static/src/components/views/theme_preview_kanban.js", "/website/static/src/services/website_service.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", "/barcodes/static/src/barcode_handler_field.js", "/barcodes/static/src/barcode_handlers.js", "/barcodes/static/src/barcode_service.js", "/barcodes/static/src/components/barcode_scanner.js", "/barcodes/static/src/float_scannable_field.js", "/barcodes/static/src/js/barcode_parser.js", "/barcodes_gs1_nomenclature/static/src/js/barcode_parser.js", "/barcodes_gs1_nomenclature/static/src/js/barcode_service.js", "/stock/static/src/client_actions/multi_print.js", "/stock/static/src/client_actions/stock_traceability_report_backend.js", "/stock/static/src/components/reception_report_line/stock_reception_report_line.js", "/stock/static/src/components/reception_report_main/stock_reception_report_main.js", "/stock/static/src/components/reception_report_table/stock_reception_report_table.js", "/stock/static/src/components/stock_overview/stock_overview.js", "/stock/static/src/fields/stock_move_line_x2_many_field.js", "/stock/static/src/picking_type_dashboard_graph/picking_type_dashboard_graph_field.js", "/stock/static/src/stock_forecasted/forecasted_buttons.js", "/stock/static/src/stock_forecasted/forecasted_details.js", "/stock/static/src/stock_forecasted/forecasted_header.js", "/stock/static/src/stock_forecasted/forecasted_warehouse_filter.js", "/stock/static/src/stock_forecasted/stock_forecasted.js", "/stock/static/src/stock_warehouse_service.js", "/stock/static/src/views/list/auto_column_width_list_renderer.js", "/stock/static/src/views/list/inventory_report_list_model.js", "/stock/static/src/views/list/inventory_report_list_view.js", "/stock/static/src/views/list/stock_report_list_view.js", "/stock/static/src/views/picking_form/stock_move_one2many.js", "/stock/static/src/views/search/stock_orderpoint_search_model.js", "/stock/static/src/views/search/stock_orderpoint_search_panel.js", "/stock/static/src/views/search/stock_report_search_model.js", "/stock/static/src/views/search/stock_report_search_panel.js", "/stock/static/src/views/stock_empty_list_help.js", "/stock/static/src/views/stock_orderpoint_list_controller.js", "/stock/static/src/views/stock_orderpoint_list_view.js", "/stock/static/src/widgets/counted_quantity_widget.js", "/stock/static/src/widgets/forecast_widget.js", "/stock/static/src/widgets/generate_serial.js", "/stock/static/src/widgets/json_widget.js", "/stock/static/src/widgets/popover_widget.js", "/stock/static/src/widgets/stock_pick_from.js", "/stock/static/src/widgets/stock_rescheduling_popover.js", "/web_enterprise/static/src/webclient/burger_menu/burger_menu.js", "/web_enterprise/static/src/webclient/color_scheme/color_scheme_menu_items.js", "/web_enterprise/static/src/webclient/color_scheme/color_scheme_service.js", "/web_enterprise/static/src/webclient/home_menu/enterprise_subscription_service.js", "/web_enterprise/static/src/webclient/home_menu/expiration_panel.js", "/web_enterprise/static/src/webclient/home_menu/home_menu.js", "/web_enterprise/static/src/webclient/home_menu/home_menu_service.js", "/web_enterprise/static/src/webclient/navbar/navbar.js", "/web_enterprise/static/src/webclient/promote_studio_dialog/promote_studio_dialog.js", "/web_enterprise/static/src/webclient/share_url/burger_menu.js", "/web_enterprise/static/src/webclient/share_url/share_url.js", "/web_enterprise/static/src/webclient/webclient.js", "/web_enterprise/static/src/views/kanban/kanban_header_patch.js", "/web_enterprise/static/src/views/list/list_renderer_desktop.js", "/web_mobile/static/src/js/core/hooks.js", "/web_mobile/static/src/js/core/mixins.js", "/web_mobile/static/src/js/core/network/download.js", "/web_mobile/static/src/js/core/popover.js", "/web_mobile/static/src/js/crash_manager.js", "/web_mobile/static/src/js/hook_event_bus.js", "/web_mobile/static/src/js/mobile_service.js", "/web_mobile/static/src/js/services/core.js", "/web_mobile/static/src/js/user_menu_items.js", "/web_mobile/static/src/views/user_preferences_form_view.js", "/mail_enterprise/static/src/core/common/message_patch.js", "/mail_enterprise/static/src/attachments/attachment_viewer_patch.js", "/mail_enterprise/static/src/web/chat_window/chat_window_patch.js", "/mail_enterprise/static/src/web/messaging_menu/messaging_menu_patch.js", "/mail_enterprise/static/src/web/thread_action_patch.js", "/account_accountant/static/src/js/tours/account_accountant.js", "/account_accountant/static/src/components/bank_reconciliation/amls_list_view.js", "/account_accountant/static/src/components/bank_reconciliation/bank_rec_quick_create.js", "/account_accountant/static/src/components/bank_reconciliation/bank_rec_record.js", "/account_accountant/static/src/components/bank_reconciliation/embedded_list_view.js", "/account_accountant/static/src/components/bank_reconciliation/finish_buttons.js", "/account_accountant/static/src/components/bank_reconciliation/global_info.js", "/account_accountant/static/src/components/bank_reconciliation/kanban.js", "/account_accountant/static/src/components/bank_reconciliation/list.js", "/account_accountant/static/src/components/bank_reconciliation/list_view_switcher.js", "/account_accountant/static/src/components/bank_reconciliation/many2one_field_multi_edit.js", "/account_accountant/static/src/components/bank_reconciliation/monetary_field_auto_signed_amount.js", "/account_accountant/static/src/components/bank_reconciliation/rainbowman_content.js", "/account_accountant/static/src/components/bank_reconciliation/view_embedder.js", "/account_accountant/static/src/components/export_data_dialog/export_data_dialog.js", "/account_accountant/static/src/components/matching_link_widget/matching_link_widget.js", "/account_accountant/static/src/components/move_line_list/attachment_view_move_line.js", "/account_accountant/static/src/components/move_line_list/move_line_list.js", "/account_accountant/static/src/components/move_line_list_reconcile/move_line_list_reconcile.js", "/accountant/static/src/js/tours/accountant.js", "/purchase/static/src/components/monetary_field_no_zero/monetary_field_no_zero.js", "/purchase/static/src/components/open_match_line_widget/open_match_line_widget.js", "/purchase/static/src/product_catalog/kanban_record.js", "/purchase/static/src/product_catalog/purchase_order_line/purchase_order_line.js", "/purchase/static/src/toaster_button/toaster_button_widget.js", "/purchase/static/src/views/purchase_dashboard.js", "/purchase/static/src/views/purchase_kanbanview.js", "/purchase/static/src/views/purchase_listview.js", "/purchase/static/src/js/tours/purchase.js", "/purchase/static/src/js/tours/purchase_steps.js", "/iap/static/src/action_buttons_widget/action_buttons_widget.js", "/iap/static/src/js/insufficient_credit_error_handler.js", "/iap_mail/static/src/js/services/iap_notification_service.js", "/sms/static/src/components/phone_field/phone_field.js", "/sms/static/src/components/sms_button/sms_button.js", "/sms/static/src/components/sms_widget/fields_sms_widget.js", "/sms/static/src/core/failure_model_patch.js", "/sms/static/src/core/notification_model_patch.js", "/sms/static/src/messaging_menu/messaging_menu_patch.js", "/sms/static/src/thread/message_patch.js", "/base_automation/static/src/base_automation_actions_one2many_field.js", "/base_automation/static/src/base_automation_error_dialog.js", "/base_automation/static/src/base_automation_trigger_selection_field.js", "/base_automation/static/src/kanban_header_patch.js", "/base_automation/static/src/utils.js", "/base_import_module/static/src/base_import_list_renderer.js", "/base_import_module/static/src/base_import_list_view.js", "/web_studio/static/src/systray_item/systray_item.js", "/web_studio/static/src/studio_service.js", "/web_studio/static/src/utils.js", "/web_studio/static/src/client_action/studio_action_loader.js", "/web_studio/static/src/client_action/app_creator/app_creator_shortcut.js", "/web_studio/static/src/client_action/components/font_awesome_icon_selector/font_awesome_icon_selector.js", "/web_studio/static/src/client_action/components/sidebar_draggable_item/sidebar_draggable_item.js", "/web_studio/static/src/client_action/components/thumbnail_item/thumbnail_item.js", "/web_studio/static/src/export/studio_export_action.js", "/web_studio/static/src/home_menu/home_menu.js", "/web_studio/static/src/views/list/list_renderer.js", "/web_studio/static/src/js/tours/web_studio_tour.js", "/web_studio/static/src/approval/approval_hook.js", "/web_studio/static/src/approval/approval_infos.js", "/web_studio/static/src/approval/form_controller.js", "/web_studio/static/src/approval/studio_approval.js", "/web_studio/static/src/approval/view_button_approval.js", "/resource_mail/static/src/components/avatar_card_resource/avatar_card_resource_popover.js", "/resource_mail/static/src/views/fields/many2many_avatar_resource/many2many_avatar_resource_field.js", "/resource_mail/static/src/views/fields/many2one_avatar_resource/many2one_avatar_resource_field.js", "/hr/static/src/components/avatar_card/avatar_card_popover_patch.js", "/hr/static/src/components/avatar_card_employee/avatar_card_employee_popover.js", "/hr/static/src/components/avatar_card_resource/avatar_card_resource_popover_patch.js", "/hr/static/src/components/background_image/background_image.js", "/hr/static/src/components/department_chart/department_chart.js", "/hr/static/src/components/employee_chat/employee_chat.js", "/hr/static/src/components/hr_presence_status/hr_presence_status.js", "/hr/static/src/components/hr_presence_status_private/hr_presence_status_private.js", "/hr/static/src/components/radio_image_field/radio_image_field.js", "/hr/static/src/components/work_permit_upload/work_permit_upload.js", "/hr/static/src/core/common/persona_model_patch.js", "/hr/static/src/core/web/thread_actions.js", "/hr/static/src/store_service_patch.js", "/hr/static/src/user_menu/my_profile.js", "/hr/static/src/views/archive_employee_hook.js", "/hr/static/src/views/fields/employee_field_relation_mixin.js", "/hr/static/src/views/fields/many2many_avatar_employee_field/many2many_avatar_employee_field.js", "/hr/static/src/views/fields/many2many_avatar_user_field/many2many_avatar_user_field_patch.js", "/hr/static/src/views/fields/many2one_avatar_employee_field/many2one_avatar_employee_field.js", "/hr/static/src/views/fields/many2one_avatar_user_field/many2one_avatar_user_field_patch.js", "/hr/static/src/views/form_view.js", "/hr/static/src/views/hr_action_helper.js", "/hr/static/src/views/list_view.js", "/hr/static/src/views/open_chat_hook.js", "/hr/static/src/views/profile_form_view.js", "/knowledge/static/src/components/article_selection_dialog/article_selection_dialog.js", "/knowledge/static/src/components/article_template_picker_dialog/article_template_picker_dialog.js", "/knowledge/static/src/components/breadcrumbs/breadcrumbs.js", "/knowledge/static/src/components/chatter_panel/chatter_panel.js", "/knowledge/static/src/components/custom_favorite_item/custom_favorite_filter.js", "/knowledge/static/src/components/embedded_view_actions_menu/embedded_view_actions_menu.js", "/knowledge/static/src/components/external_embedded_view_favorite_menu/embedded_view_favorite_menu.js", "/knowledge/static/src/components/external_embedded_view_insertion/views_renderers_patches.js", "/knowledge/static/src/components/form_status_indicator/form_status_indicator.js", "/knowledge/static/src/components/hierarchy/hierarchy.js", "/knowledge/static/src/components/item_calendar_props_dialog/item_calendar_props_dialog.js", "/knowledge/static/src/components/knowledge_cover/knowledge_cover.js", "/knowledge/static/src/components/knowledge_cover/knowledge_cover_dialog.js", "/knowledge/static/src/components/knowledge_dropdown/knowledge_dropdown_patch.js", "/knowledge/static/src/components/knowledge_html_field/knowledge_html_field.js", "/knowledge/static/src/components/knowledge_html_viewer/knowledge_html_viewer.js", "/knowledge/static/src/components/knowledge_icon/knowledge_icon.js", "/knowledge/static/src/components/knowledge_wysiwyg/knowledge_wysiwyg.js", "/knowledge/static/src/components/move_article_dialog/move_article_dialog.js", "/knowledge/static/src/components/permission_panel/permission_panel.js", "/knowledge/static/src/components/prompt_embedded_view_name_dialog/prompt_embedded_view_name_dialog.js", "/knowledge/static/src/components/properties_panel/properties_panel.js", "/knowledge/static/src/components/sidebar/sidebar.js", "/knowledge/static/src/components/sidebar/sidebar_row.js", "/knowledge/static/src/components/sidebar/sidebar_section.js", "/knowledge/static/src/components/topbar/topbar.js", "/knowledge/static/src/components/with_lazy_loading/with_lazy_loading.js", "/knowledge/static/src/components/with_sub_env/with_sub_env.js", "/knowledge/static/src/components/wysiwyg_article_helper/wysiwyg_article_helper.js", "/knowledge/static/src/editor/embedded_components/backend/article_index/article_index.js", "/knowledge/static/src/editor/embedded_components/backend/clipboard/macros_embedded_clipboard.js", "/knowledge/static/src/editor/embedded_components/backend/embedded_view_link/embedded_view_link.js", "/knowledge/static/src/editor/embedded_components/backend/embedded_view_link/embedded_view_link_edit_dialog.js", "/knowledge/static/src/editor/embedded_components/backend/embedded_view_link/embedded_view_link_popover.js", "/knowledge/static/src/editor/embedded_components/backend/embedded_view_link/readonly_embedded_view_link.js", "/knowledge/static/src/editor/embedded_components/backend/file/macros_file.js", "/knowledge/static/src/editor/embedded_components/backend/file/macros_file_mixin.js", "/knowledge/static/src/editor/embedded_components/backend/file/readonly_macros_file.js", "/knowledge/static/src/editor/embedded_components/backend/view/embedded_view.js", "/knowledge/static/src/editor/embedded_components/backend/view/readonly_embedded_view.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/embedded_components/embedding_sets.js", "/knowledge/static/src/editor/embedded_components/plugins/article_index_plugin/article_index_plugin.js", "/knowledge/static/src/editor/embedded_components/plugins/embedded_clipboard_plugin/embedded_clipboard_plugin.js", "/knowledge/static/src/editor/embedded_components/plugins/embedded_view_link_plugin/embedded_view_link_plugin.js", "/knowledge/static/src/editor/embedded_components/plugins/embedded_view_plugin/embedded_view_plugin.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", "/knowledge/static/src/editor/plugin_sets.js", "/knowledge/static/src/editor/plugins/article_plugin/article_plugin.js", "/knowledge/static/src/editor/plugins/autofocus_plugin/autofocus_plugin.js", "/knowledge/static/src/editor/plugins/comments_plugin/comments_plugin.js", "/knowledge/static/src/editor/plugins/insert_pending_element_plugin/insert_pending_element_plugin.js", "/knowledge/static/src/comments/comment/comment.js", "/knowledge/static/src/comments/comment/comment_anchor_text.js", "/knowledge/static/src/comments/comment_beacon_manager.js", "/knowledge/static/src/comments/comments_handler/comments_handler.js", "/knowledge/static/src/comments/comments_panel/comments_panel.js", "/knowledge/static/src/comments/comments_popover/comments_popover.js", "/knowledge/static/src/comments/comments_service.js", "/knowledge/static/src/comments/editor_thread_info.js", "/knowledge/static/src/mail/composer/composer.js", "/knowledge/static/src/mail/composer/composer_patch.js", "/knowledge/static/src/mail/emoji_picker/emoji_picker_patch.js", "/knowledge/static/src/mail/message/knowledge_message.js", "/knowledge/static/src/mail/message/message_actions.js", "/knowledge/static/src/mail/message/message_model_patch.js", "/knowledge/static/src/mail/message/message_patch.js", "/knowledge/static/src/mail/thread/knowledge_thread.js", "/knowledge/static/src/mail/thread/thread_actions_patch.js", "/knowledge/static/src/mail/thread/thread_model_patch.js", "/knowledge/static/src/search_model/search_model.js", "/knowledge/static/src/web/chatter_patch.js", "/knowledge/static/src/js/knowledge_controller.js", "/knowledge/static/src/js/knowledge_utils.js", "/knowledge/static/src/js/knowledge_renderers.js", "/knowledge/static/src/js/knowledge_views.js", "/knowledge/static/src/webclient/commands/command_category.js", "/knowledge/static/src/webclient/commands/knowledge_providers.js", "/knowledge/static/src/views/embedded_controllers_patch.js", "/knowledge/static/src/views/embedded_kanban_view.js", "/knowledge/static/src/views/embedded_list_view.js", "/knowledge/static/src/views/embedded_view.js", "/knowledge/static/src/views/item_calendar/item_calendar_model.js", "/knowledge/static/src/views/item_calendar/item_calendar_view.js", "/knowledge/static/src/views/list_view.js", "/knowledge/static/src/services/knowledge_commands_service.js", "/knowledge/static/src/services/knowledge_embedded_filters_service.js", "/knowledge/static/src/macros/abstract_macro.js", "/knowledge/static/src/macros/clipboard_macros.js", "/knowledge/static/src/macros/file_macros.js", "/knowledge/static/src/macros/utils.js", "/odoo_direct_print/static/lib/print.min.js", "/odoo_direct_print/static/src/js/browser_print_dialog.js", "/sign/static/src/js/sign_datetime_widget.js", "/sign/static/src/js/tours/sign.js", "/sign/static/src/activity/activity_list_popover_item_patch.js", "/sign/static/src/activity/activity_model_patch.js", "/sign/static/src/activity/activity_patch.js", "/sign/static/src/mixin/document_upload.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/backend_components/cog_menu/sign_request_cog_menu.js", "/sign/static/src/backend_components/editable_pdf_iframe_mixin.js", "/sign/static/src/backend_components/multi_file_upload.js", "/sign/static/src/backend_components/sign_request/edit_while_signing_signable_pdf_iframe.js", "/sign/static/src/backend_components/sign_request/sign_request_action.js", "/sign/static/src/backend_components/sign_request/sign_request_control_panel.js", "/sign/static/src/backend_components/sign_request/signable_sign_request_action.js", "/sign/static/src/backend_components/sign_request/signable_sign_request_control_panel.js", "/sign/static/src/backend_components/sign_template/sign_item_custom_popover.js", "/sign/static/src/backend_components/sign_template/sign_template_action.js", "/sign/static/src/backend_components/sign_template/sign_template_body.js", "/sign/static/src/backend_components/sign_template/sign_template_control_panel.js", "/sign/static/src/backend_components/sign_template/sign_template_iframe.js", "/sign/static/src/backend_components/sign_template/sign_template_top_bar.js", "/sign/static/src/backend_components/template_alert_dialog/template_alert_dialog.js", "/sign/static/src/views/helper/sign_action_helper.js", "/sign/static/src/views/hooks.js", "/sign/static/src/views/sign_kanban/sign_kanban_controller.js", "/sign/static/src/views/sign_kanban/sign_kanban_renderer.js", "/sign/static/src/views/sign_kanban/sign_kanban_view.js", "/sign/static/src/views/sign_list/sign_list_controller.js", "/sign/static/src/views/sign_list/sign_list_view.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", "/sign/static/src/fields/ many2many_binary_field/ many2many_binary_field.js", "/sign/static/src/fields/signer_x2many.js", "/rating/static/src/core/common/message_model_patch.js", "/rating/static/src/core/common/rating_model.js", "/rating/static/src/core/web/notification_item_patch.js", "/helpdesk/static/src/components/helpdesk_sla_many2many_tags/helpdesk_sla_many2many_tags.js", "/helpdesk/static/src/components/helpdesk_sla_tags_list/helpdesk_sla_tags_list.js", "/helpdesk/static/src/components/helpdesk_team_dashboard/helpdesk_team_dashboard.js", "/helpdesk/static/src/components/helpdesk_team_target/helpdesk_team_target.js", "/helpdesk/static/src/components/helpdesk_ticket_smiley_badge/helpdesk_ticket_smiley_badge.js", "/helpdesk/static/src/views/helpdesk_team_form/helpdesk_team_form_controller.js", "/helpdesk/static/src/views/helpdesk_team_form/helpdesk_team_form_view.js", "/helpdesk/static/src/views/helpdesk_team_kanban/helpdesk_team_kanban_controller.js", "/helpdesk/static/src/views/helpdesk_team_kanban/helpdesk_team_kanban_view.js", "/helpdesk/static/src/views/helpdesk_ticket_kanban/helpdesk_ticket_kanban_header.js", "/helpdesk/static/src/views/helpdesk_ticket_kanban/helpdesk_ticket_kanban_renderer.js", "/helpdesk/static/src/views/helpdesk_ticket_kanban/helpdesk_ticket_kanban_view.js", "/helpdesk/static/src/views/helpdesk_ticket_list/helpdesk_ticket_list_renderer.js", "/helpdesk/static/src/views/helpdesk_ticket_list/helpdesk_ticket_list_view.js", "/helpdesk/static/src/js/tours/helpdesk.js", "/stock_barcode/static/src/barcode_object.js", "/stock_barcode/static/src/components/apply_quant_dialog.js", "/stock_barcode/static/src/components/backorder_dialog.js", "/stock_barcode/static/src/components/count_screen_rfid.js", "/stock_barcode/static/src/components/grouped_line.js", "/stock_barcode/static/src/components/line.js", "/stock_barcode/static/src/components/main.js", "/stock_barcode/static/src/components/manual_barcode.js", "/stock_barcode/static/src/components/package_line.js", "/stock_barcode/static/src/components/product_image_dialog.js", "/stock_barcode/static/src/kanban/stock_barcode_kanban_controller.js", "/stock_barcode/static/src/kanban/stock_barcode_kanban_renderer.js", "/stock_barcode/static/src/kanban/stock_barcode_kanban_view.js", "/stock_barcode/static/src/lazy_barcode_cache.js", "/stock_barcode/static/src/main_menu/main_menu.js", "/stock_barcode/static/src/models/barcode_model.js", "/stock_barcode/static/src/models/barcode_picking_model.js", "/stock_barcode/static/src/models/barcode_quant_model.js", "/stock_barcode/static/src/widgets/digipad.js", "/stock_barcode/static/src/widgets/image_preview.js", "/stock_barcode/static/src/widgets/set_reserved_qty_button.js", "/hr_skills/static/src/fields/skills_one2many/skills_one2many.js", "/hr_skills/static/src/fields/auto_save_skill_type/auto_save_skill_type.js", "/hr_skills/static/src/fields/boolean_toggle_load/boolean_toggle_load.js", "/hr_skills/static/src/fields/hr_skills_tags_list/hr_skills_tags_list.js", "/hr_skills/static/src/fields/many2many_tags_skills/many2many_tags_skills.js", "/hr_skills/static/src/fields/resume_one2many/resume_one2many.js", "/hr_skills/static/src/views/skills_list_renderer.js", "/hr_skills/static/src/components/avatar_card_resource/avatar_card_resource_popover_patch.js", "/stock_account/static/src/fields/boolean_confirm.js", "/stock_account/static/src/stock_account_forecasted/forecasted_header.js", "/account_reports/static/src/components/account_report/account_report.js", "/account_reports/static/src/components/account_report/buttons_bar/buttons_bar.js", "/account_reports/static/src/components/account_report/cog_menu/cog_menu.js", "/account_reports/static/src/components/account_report/controller.js", "/account_reports/static/src/components/account_report/ellipsis/ellipsis.js", "/account_reports/static/src/components/account_report/ellipsis/popover/ellipsis_popover.js", "/account_reports/static/src/components/account_report/filters/filters.js", "/account_reports/static/src/components/account_report/header/header.js", "/account_reports/static/src/components/account_report/line/line.js", "/account_reports/static/src/components/account_report/line/popover/debug_popover.js", "/account_reports/static/src/components/account_report/line_cell/line_cell.js", "/account_reports/static/src/components/account_report/line_cell/popover/carryover_popover.js", "/account_reports/static/src/components/account_report/line_cell/popover/edit_popover.js", "/account_reports/static/src/components/account_report/line_cell_editable/line_cell_editable.js", "/account_reports/static/src/components/account_report/line_name/line_name.js", "/account_reports/static/src/components/account_report/line_name/popover/annotations_popover.js", "/account_reports/static/src/components/account_report/line_name/popover_line/annotation_popover_line.js", "/account_reports/static/src/components/account_report/search_bar/search_bar.js", "/account_reports/static/src/components/aged_partner_balance/filters.js", "/account_reports/static/src/components/journal_dashboard_activity/journal_dashboard_activity.js", "/account_reports/static/src/components/journal_report/filters.js", "/account_reports/static/src/components/journal_report/line/line.js", "/account_reports/static/src/components/journal_report/move_line_list/move_line_list.js", "/account_reports/static/src/components/multicurrency_revaluation_report/filters/filters.js", "/account_reports/static/src/components/partner_ledger/line_cell.js", "/account_reports/static/src/components/redirectAction/redirectAction.js", "/account_reports/static/src/components/sales_report/filters/filters.js", "/account_reports/static/src/js/action_manager_account_report_dl.js", "/account_reports/static/src/js/util.js", "/account_reports/static/src/widgets/account_report_x2many/account_report_x2many.js", "/account_asset/static/src/components/move_reversed/move_reversed.js", "/base_import/static/src/binary_file_manager.js", "/base_import/static/src/import_action/import_action.js", "/base_import/static/src/import_block_ui.js", "/base_import/static/src/import_data_column_error/import_data_column_error.js", "/base_import/static/src/import_data_content/import_data_content.js", "/base_import/static/src/import_data_options/import_data_options.js", "/base_import/static/src/import_data_progress/import_data_progress.js", "/base_import/static/src/import_data_sidepanel/import_data_sidepanel.js", "/base_import/static/src/import_model.js", "/base_import/static/src/import_records/import_records.js", "/account_loans/static/src/components/loans/file_upload.js", "/account_loans/static/src/components/loans/import_action.js", "/purchase_stock/static/src/js/tours/purchase_stock.js", "/account_bank_statement_import/static/src/account_bank_statement_import_model.js", "/account_bank_statement_import/static/src/bank_reconciliation/finish_buttons.js", "/account_bank_statement_import/static/src/bank_reconciliation/kanban.js", "/account_bank_statement_import/static/src/bank_reconciliation/list.js", "/iap_extract/static/src/components/status_header/status.js", "/account_bank_statement_import_csv/static/src/bank_statement_csv_import_action.js", "/account_bank_statement_import_csv/static/src/bank_statement_csv_import_model.js", "/account_base_import/static/src/js/account_import_action.js", "/account_base_import/static/src/js/account_import_guide.js", "/account_base_import/static/src/js/account_import_model.js", "/account_followup/static/src/components/change_trust_widget/followup_trust_widget.js", "/account_followup/static/src/components/download_close_wizard_action/download_close_wizard_action.js", "/account_invoice_extract/static/src/js/box.js", "/account_invoice_extract/static/src/js/box_layer.js", "/account_invoice_extract/static/src/js/invoice_extract_form_renderer.js", "/account_invoice_extract/static/src/js/invoice_extract_form_view.js", "/account_online_synchronization/static/src/components/account_duplicate_transaction/account_duplicate_transaction_form.js", "/account_online_synchronization/static/src/components/account_duplicate_transaction/account_duplicate_transaction_hook.js", "/account_online_synchronization/static/src/components/account_duplicate_transaction/account_duplicate_transaction_service.js", "/account_online_synchronization/static/src/components/account_duplicate_transaction/account_duplicate_transactions_x2many.js", "/account_online_synchronization/static/src/components/bank_configure/bank_configure.js", "/account_online_synchronization/static/src/components/bank_reconciliation/fetch_missing_transactions_cog_menu.js", "/account_online_synchronization/static/src/components/bank_reconciliation/find_duplicate_transactions_cog_menu.js", "/account_online_synchronization/static/src/components/connected_until_widget/connected_until_widget.js", "/account_online_synchronization/static/src/components/online_account_radio/online_account_radio.js", "/account_online_synchronization/static/src/components/refresh_spin_journal_widget/refresh_spin_journal_widget.js", "/account_online_synchronization/static/src/components/transient_bank_statement_line_list_view/transient_bank_statement_line_list_view.js", "/account_online_synchronization/static/src/components/views/account_online_authorization_kanban.js", "/account_online_synchronization/static/src/js/odoo_fin_connector.js", "/crm_enterprise/static/src/activity_menu_patch.js", "/crm_iap_mine/static/src/js/tours/crm_iap_lead.js", "/export_view_pdf/static/src/js/export_dialog.js", "/export_view_pdf/static/src/js/pdf_export.js", "/hr_calendar/static/src/calendar/calendar_model.js", "/hr_calendar/static/src/calendar/common/calendar_common_renderer.js", "/hr_calendar/static/src/calendar/filter_panel/calendar_filter_panel.js", "/hr_calendar/static/src/views/fields/many2many_attendees.js", "/hr_mobile/static/src/js/language_mobile.js", "/hr_org_chart/static/src/fields/hooks.js", "/hr_org_chart/static/src/fields/hr_org_chart.js", "/loyalty/static/src/js/loyalty_card_list_view.js", "/loyalty/static/src/js/loyalty_control_panel_widget.js", "/loyalty/static/src/js/loyalty_list_view.js", "/mail_mobile/static/src/js/mobile_device_register.js", "/muk_web_chatter/static/src/core/thread/thread.js", "/muk_web_chatter/static/src/views/form/form_renderer.js", "/muk_web_enterprise_theme/static/src/webclient/home_menu/home_menu.js", "/partner_autocomplete/static/src/js/partner_autocomplete_component.js", "/partner_autocomplete/static/src/js/partner_autocomplete_core.js", "/partner_autocomplete/static/src/js/partner_autocomplete_fieldchar.js", "/partner_autocomplete/static/src/js/partner_autocomplete_many2one.js", "/partner_autocomplete/static/src/js/web_company_autocomplete.js", "/product_barcodelookup/static/src/widgets/barcode_scanner.js", "/product_matrix/static/src/js/product_matrix_dialog.js", "/sale_account_accountant/static/src/components/bank_reconciliation/kanban.js", "/sale_loyalty/static/src/views/fields/loyalty_total/loyalty_data_field.js", "/sale_pdf_quote_builder/static/src/js/custom_content_kanban_like_widget/custom_content_kanban_like_widget.js", "/sale_pdf_quote_builder/static/src/js/custom_content_kanban_like_widget/custom_field_card/custom_field_card.js", "/sale_pdf_quote_builder/static/src/js/quotation_document_kanban/quotation_document_kanban_controller.js", "/sale_pdf_quote_builder/static/src/js/quotation_document_kanban/quotation_document_kanban_view.js", "/sale_pdf_quote_builder/static/src/js/quotation_document_kanban/quotation_document_kanban_widget.js", "/sale_product_matrix/static/src/js/sale_product_field.js", "/sale_stock/static/src/product_catalog/kanban_record.js", "/sale_stock/static/src/product_catalog/sale_order_line/sale_order_line.js", "/sale_stock/static/src/sale_stock_forecasted/forecasted_details.js", "/sale_stock/static/src/widgets/qty_at_date_widget.js", "/snailmail/static/src/core/failure_model_patch.js", "/snailmail/static/src/core/notification_model_patch.js", "/snailmail/static/src/core_ui/message_patch.js", "/snailmail/static/src/core_ui/snailmail_error.js", "/snailmail/static/src/core_ui/snailmail_notification_popover.js", "/snailmail/static/src/messaging_menu/messaging_menu_patch.js", "/spreadsheet/static/src/assets_backend/constants.js", "/spreadsheet/static/src/assets_backend/helpers.js", "/spreadsheet/static/src/assets_backend/spreadsheet_action_loader.js", "/spreadsheet/static/src/assets_backend/spreadsheet_binary_field/spreadsheet_binary_field.js", "/spreadsheet_dashboard/static/src/assets/dashboard_action_loader.js", "/spreadsheet_edition/static/src/assets/components/many2one_spreadsheet_field.js", "/spreadsheet_edition/static/src/assets/components/spreadsheet_selector_dialog/spreadsheet_selector_dialog.js", "/spreadsheet_edition/static/src/assets/components/spreadsheet_selector_dialog/spreadsheet_selector_panel.js", "/spreadsheet_edition/static/src/assets/components/spreadsheet_selector_grid/spreadsheet_selector_grid.js", "/spreadsheet_edition/static/src/assets/insert_action_link_menu/insert_action_link_menu.js", "/spreadsheet_edition/static/src/assets/list_view/insert_list_spreadsheet_menu_owl.js", "/spreadsheet_edition/static/src/assets/list_view/list_controller.js", "/spreadsheet_edition/static/src/assets/list_view/list_renderer.js", "/spreadsheet_edition/static/src/assets/message_model_patch.js", "/spreadsheet_edition/static/src/assets/spreadsheet_cog_menu/spreadsheet_cog_menu.js", "/spreadsheet_edition/static/src/assets/spreadsheet_history_action_loader.js", "/spreadsheet_dashboard_edition/static/src/assets/dashboard_edit_action_loader.js", "/spreadsheet_sale_management/static/src/assets/field_sync_action_loader.js", "/stock_barcode_barcodelookup/static/src/components/main.js", "/stock_barcode_barcodelookup/static/src/models/barcode_model.js", "/stock_barcode_barcodelookup/static/src/models/barcode_picking_model.js", "/stock_barcode_picking_batch/static/src/components/grouped_line.js", "/stock_barcode_picking_batch/static/src/components/line.js", "/stock_barcode_picking_batch/static/src/components/main.js", "/stock_barcode_picking_batch/static/src/components/option_line.js", "/stock_barcode_picking_batch/static/src/kanban/stock_barcode_kanban_controller.js", "/stock_barcode_picking_batch/static/src/kanban/stock_barcode_kanban_renderer.js", "/stock_barcode_picking_batch/static/src/lazy_barcode_cache.js", "/stock_barcode_picking_batch/static/src/models/barcode_picking_batch_model.js", "/website_enterprise/static/src/client_actions/configurator/configurator.js", "/website_knowledge/static/src/backend/client_actions/website_preview/website_preview.js", "/website_knowledge/static/src/backend/components/permission_panel/permission_panel.js", "/sale_commission/static/src/js/commission_plan_graph/commission_plan_graph.js", "/web_enterprise/static/src/main.js", "/web/static/src/start.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;ACpPA;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/kMA;;;;;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;;;;ACvMA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;;;;AC9NA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;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;;;;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;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;;;;ACldA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpQA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;ACpsBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;;;;ACpVA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1iBA;;;;;;;;AAAA;AACA;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;;;;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;;;;AC3LA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpnBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;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;;;;AC1VA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;;;;AC9jBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClQA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACn4BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpVA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;;;;AC/KA;;;;;;;;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;;;;AC1OA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvlCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9VA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClPA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjMA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC3hDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpOA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxLA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5WA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AClLA;;;;;;;;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;;;;ACxIA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClwCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5sBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtgCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACv0BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACz0BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtnBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChrBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtMA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC15EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC5DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AChBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACh2BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACtOA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;;;;ACrFA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1YA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;AClDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACjGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;ACvCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACnFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACVA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;ACxCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACjIA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACtBA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACnLA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;ACvBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/XA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClKA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;;;;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;;;;ACpDA;;;;;;;;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;;;;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;;;;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;;;;ACrMA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;AC/GA;;;;;;;;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;;;;AC7JA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC77BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChSA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnVA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzWA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACpQA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACx2BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;AACA;AACA;AACA;;;;ACpLA;;;;;;;;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;;;;AClEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1GA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvWA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;ACjEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;ACjDA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzsBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChrBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnMA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;AC5IA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;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;;;;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;;;;ACxBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACVA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;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;;;;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;;;;ACrGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5VA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;AC/SA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/kBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;ACnPA;;;;;;;;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;;;;ACnDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1qBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACt3DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzSA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpJA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACvDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvaA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACxDA;;;;;;;;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;;;;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;;;;AC9EA;;;;;;;;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;;;;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;;;;AC5IA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9uDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACnLA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACRA;;;;;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACVA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;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;;;;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;;;;AC3OA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;ACzBA;;;;;;;;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;;;;ACxBA;;;;;;;;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;;;;AC1DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACRA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACnGA;;;;;;;;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;;;;AC/HA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpJA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;ACvEA;;;;;;;;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;;;;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;;;;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;;;;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;;;;ACpJA;;;;;;;;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;;;;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;;;;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;;;;ACrSA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;ACjCA;;;;;;;;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;;;;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;;;;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;;;;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;;;;ACxDA;;;;;;;;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;;;;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;;;;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;;;;ACnFA;;;;;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;;;;ACvgBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrMA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7UA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvaA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;AC5KA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnUA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACPA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC3yDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrtFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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;;;;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;;;;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;;;;AC1KA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChvBA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5yCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC9nBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrkBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3pCA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5IA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3LA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACx4BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC1TA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC1OA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;AChGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACPA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;ACrJA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;ACnMA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChYA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7HA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjgBA;;;;;;;;AAAA;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;;;;AC1GA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;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;;;;AClGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACVA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvTA;;;;;;;;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;;;;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACz3BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxUA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACzDA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvWA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrVA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxVA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;AClKA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChxBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3TA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChMA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvrBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACt1BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChTA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpmBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;AClEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;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;;;;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;;;;AC5CA;;;;;;;;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;;;;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;;;;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;;;;ACzKA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvLA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;ACtFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9LA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrTA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxuBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxkBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnPA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;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;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;;;;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;;;;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;;;;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;;;;AClFA;;;;;;;;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;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5KA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9LA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjzBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;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;;;;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;;;;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;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1RA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxhBA;;;;;;;;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;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;;;;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;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;;;;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;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;;;;AClKA;;;;;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1wBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpUA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7lBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACllCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACVA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjJA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;ACjGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/HA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;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;;;;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;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;;;;ACrGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnNA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACtBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACRA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;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;;;;ACvDA;;;;;;;;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;;;;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;;;;AC/EA;;;;;;;;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;;;;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;;;;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;;;;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;;;;ACtDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;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;;;;ACnGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;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;;;;AC/LA;;;;;;;;AAAA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;ACrTA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACrDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxKA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7HA;;;;;;;;AAAA;AACA;AACA;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;;;;AChBA;;;;;;;;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;;;;ACzZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACVA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;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;;;;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;;;;ACjCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnKA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACj4DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;ACrDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACRA;;;;;;;;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;;;;AChBA;;;;;;;;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;;;;AC5LA;;;;;;;;AAAA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;;;;ACxIA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;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;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;AACA;AACA;;;;ACtBA;;;;;;;;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;;;;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;;;;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;;;;ACjJA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;AC3BA;;;;;;;;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;;;;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;;;;ACzTA;;;;;;;;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;;;;ACVA;;;;;;;;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;;;;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;;;;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;;;;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;;;;AC/JA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;;;;ACrNA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC7CA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;ACtJA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACr4BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1jDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;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;;;;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;;;;ACzSA;;;;;;;;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;;;;AC7BA;;;;;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;ACPA;;;;;;;;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;;;;ACpBA;;;;;;;;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;;;;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;;;;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;;;;ACzYA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;;;;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;;;;ACzBA;;;;;;;;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;;;;ACtBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;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;;;;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;;;;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;;;;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;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;;;;AC/DA;;;;;;;;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;;;;ACjCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;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;;;;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;;;;ACjIA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjrBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;ACtWA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChkBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3xGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;AACA;AACA;AACA;;;;AChZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC/SA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACxQA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxPA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACx0DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpxBA;;;;;;;;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;;;;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;;;;ACxDA;;;;;;;;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;;;;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;;;;AChEA;;;;;;;;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;;;;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;;;;AChDA;;;;;;;;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;;;;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;;;;ACjGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtFA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;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;;;;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;;;;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;;;;ACpDA;;;;;;;;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;;;;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;;;;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;;;;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;;;;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;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;ACrCA;;;;;;;;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;;;;AC7BA;;;;;;;;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;;;;ACtBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnPA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;AC/KA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;AC/BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;AC7CA;;;;;;;;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;;;;ACdA;;;;;;;;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;;;;ACzCA;;;;;;;;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;;;;ACbA;;;;;;;;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;;;;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;;;;AChCA;;;;;;;;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;;;;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;;;;AChCA;;;;;;;;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;;;;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;;;;AChKA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvpCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;ACpBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACbA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpPA;;;;;;;;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;;;;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;;;;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;;;;ACzEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACdA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;AC1YA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACXA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACVA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACVA;;;;;;;;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;;;;ACZA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;AC/CA;;;;;;;;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;;;;ACxIA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACfA;;;;;;;;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;;;;AC9IA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACnBA;;;;;;;;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;;;;AC3CA;;;;;;;;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;;;;ACzEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACPA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;AC1CA;;;;;;;;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;;;;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;;;;ACxGA;;;;;;;;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;;;;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;;;;ACnCA;;;;;;;;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;;;;ACrCA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClTA;;;;;;;;AAAA;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;;;;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;;;;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;;;;ACpDA;;;;;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7SA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC9IA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;AClDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;AC5IA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;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;;;;ACjHA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChTA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;AC5PA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACpBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACbA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5lBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9jBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACp3BA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9iBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;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;;;;ACvTA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;ACtGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;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;;;;AClEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3SA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpgBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;ACjGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxIA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7CA;;;;;;;;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;;;;ACpTA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrLA;;;;;;;;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;;;;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;;;;ACxJA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxKA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;ACnJA;;;;;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;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;;;;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;;;;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;;;;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;;;;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;;;;AC/CA;;;;;;;;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;;;;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;;;;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;;;;ACrMA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzXA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;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;;;;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;;;;AC1CA;;;;;;;;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;;;;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;;;;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;;;;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;;;;AClGA;;;;;;;;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;;;;ACxBA;;;;;;;;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;;;;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;;;;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;;;;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;;;;ACtCA;;;;;;;;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;;;;ACPA;;;;;;;;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;;;;ACdA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACfA;;;;;;;;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;;;;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;;;;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;;;;AC1BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3tCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACvCA;;;;;;;;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;;;;ACtCA;;;;;;;;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;;;;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;;;;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;;;;AC/CA;;;;;;;;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;;;;ACjIA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;AC9CA;;;;;;;;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;;;;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;;;;AChCA;;;;;;;;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;;;;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;;;;ACvIA;;;;;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;ACzBA;;;;;;;;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;;;;AClCA;;;;;;;;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;;;;ACjKA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;AC7BA;;;;;;;;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;;;;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;;;;ACpcA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3BA;;;;;;;;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;;;;ACrCA;;;;;;;;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;;;;ACvBA;;;;;;;;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;;;;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;;;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;;;;AC1MA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5SA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;ACpCA;;;;;;;;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;;;;ACzCA;;;;;;;;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;;;;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;;;;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;;;;AC7EA;;;;;;;;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;;;;ACbA;;;;;;;;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;;;;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;;;;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;;;;ACvDA;;;;;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;AC1BA;;;;;;;;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;;;;AC/BA;;;;;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;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;;;;AC3CA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrSA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;AC1BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;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;;;;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;;;;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;;;;AC/HA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxVA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACtDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACl5BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;ACzBA;;;;;;;;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;;;;AC/LA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;AClDA;;;;;;;;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;;;;ACxEA;;;;;;;;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;;;;AC3FA;;;;;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1TA;;;;;;;;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;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;;;;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;;;;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;;;;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;;;;AC5JA;;;;;;;;AAAA;AACA;;;;ACDA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;ACvbA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnMA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;AC/BA;;;;;;;;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;;;;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;;;;ACxBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;AC7BA;;;;;;;;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;;;;AC1BA;;;;;;;;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;;;;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;;;;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;;;;AClQA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACjFA;;;;;;;;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;;;;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;;;;AClKA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;AClMA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1OA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC7LA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;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;;;;AC1IA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;;;;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;;;;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;;;;AC9GA;;;;;;;;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;;;;AC1BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACbA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrOA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACxKA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClzBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjYA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;ACjGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7HA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC/LA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtwBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;AC1BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnHA;;;;;;;;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;;;;AC7BA;;;;;;;;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;;;;AC/BA;;;;;;;;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;;;;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;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;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;;;;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;;;;AC9BA;;;;;;;;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;;;;ACrDA;;;;;;;;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;;;;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;;;;AC/BA;;;;;;;;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;;;;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;AACA;AACA;;;;ACxGA;;;;;;;;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;;;;AC/FA;;;;;;;;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;;;;ACZA;;;;;;;;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;;;;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;;;;AC5BA;;;;;;;;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;;;;ACdA;;;;;;;;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;;;;ACxJA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;ACtBA;;;;;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7lBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;AC5DA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5uBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;AC9HA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;AC1BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;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;;;;AC/BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpzBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpoBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/IA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;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;;;;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;;;;AChEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;AC7KA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxIA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;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;;;;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;;;;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;;;;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;;;;ACvBA;;;;;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1VA;;;;;;;;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;;;;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;;;;AC5PA;;;;;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACn2BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;ACVA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;ACpDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpWA;;;;;;;;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;;;;ACrCA;;;;;;;;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;;;;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;;;;AClDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;ACnDA;;;;;;;;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;;;;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;;;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;;;;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;;;;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;;;;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;;;;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;;;;ACfA;;;;;;;;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;;;;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;;;;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;;;;AChIA;;;;;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClTA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC9GA;;;;;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;ACzBA;;;;;;;;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;;;;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;;;;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;;;;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;;;;AC9BA;;;;;;;;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;;;;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;;;;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;;;;AC1BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;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;;;;ACxCA;;;;;;;;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;;;;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;;;;ACxBA;;;;;;;;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;;;;ACnDA;;;;;;;;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;;;;ACHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;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;;;;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;;;;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;;;;AC/DA;;;;;;;;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;;;;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;;;;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;;;;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;;;;AC9BA;;;;;;;;AAAA;AACA;AACA;AACA;;;;ACHA;;;;;;;;AAAA;AACA;AACA;AACA;;;;ACHA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;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;;;;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;;;;AC3DA;;;;;;;;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;;;;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;;;;ACvCA;;;;;;;;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;;;;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;;;;ACvHA;;;;;;;;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", "sourcesContent": ["// @odoo-module ignore\n\n//-----------------------------------------------------------------------------\n// Odoo Web Boostrap Code\n//-----------------------------------------------------------------------------\n\n(function (odoo) {\n    \"use strict\";\n\n    if (odoo.loader) {\n        // Allows for duplicate calls to `module_loader`: only the first one is\n        // executed.\n        return;\n    }\n\n    class ModuleLoader {\n        /** @type {OdooModuleLoader[\"bus\"]} */\n        bus = new EventTarget();\n        /** @type {OdooModuleLoader[\"checkErrorProm\"]} */\n        checkErrorProm = null;\n        /** @type {OdooModuleLoader[\"factories\"]} */\n        factories = new Map();\n        /** @type {OdooModuleLoader[\"failed\"]} */\n        failed = new Set();\n        /** @type {OdooModuleLoader[\"jobs\"]} */\n        jobs = new Set();\n        /** @type {OdooModuleLoader[\"modules\"]} */\n        modules = new Map();\n\n        /**\n         * @param {HTMLElement} [root]\n         */\n        constructor(root) {\n            this.root = root;\n        }\n\n        /** @type {OdooModuleLoader[\"addJob\"]} */\n        addJob(name) {\n            this.jobs.add(name);\n            this.startModules();\n        }\n\n        /** @type {OdooModuleLoader[\"define\"]} */\n        define(name, deps, factory, lazy = false) {\n            if (typeof name !== \"string\") {\n                throw new Error(`Module name should be a string, got: ${String(name)}`);\n            }\n            if (!Array.isArray(deps)) {\n                throw new Error(\n                    `Module dependencies should be a list of strings, got: ${String(deps)}`\n                );\n            }\n            if (typeof factory !== \"function\") {\n                throw new Error(`Module factory should be a function, got: ${String(factory)}`);\n            }\n            if (this.factories.has(name)) {\n                return; // Ignore duplicate modules\n            }\n            this.factories.set(name, {\n                deps,\n                fn: factory,\n                ignoreMissingDeps: globalThis.__odooIgnoreMissingDependencies,\n            });\n            if (!lazy) {\n                this.addJob(name);\n                this.checkErrorProm ||= Promise.resolve().then(() => {\n                    this.checkErrorProm = null;\n                    this.reportErrors(this.findErrors());\n                });\n            }\n        }\n\n        /** @type {OdooModuleLoader[\"findErrors\"]} */\n        findErrors(moduleNames) {\n            /**\n             * @param {Iterable<string>} currentModuleNames\n             * @param {Set<string>} visited\n             * @returns {string | null}\n             */\n            const findCycle = (currentModuleNames, visited) => {\n                for (const name of currentModuleNames || []) {\n                    if (visited.has(name)) {\n                        const cycleModuleNames = [...visited, name];\n                        return cycleModuleNames\n                            .slice(cycleModuleNames.indexOf(name))\n                            .map((j) => `\"${j}\"`)\n                            .join(\" => \");\n                    }\n                    const cycle = findCycle(dependencyGraph[name], new Set(visited).add(name));\n                    if (cycle) {\n                        return cycle;\n                    }\n                }\n                return null;\n            };\n\n            moduleNames ||= this.jobs;\n\n            /** @type {Record<string, Iterable<string>>} */\n            const dependencyGraph = Object.create(null);\n            /** @type {Set<string>} */\n            const missing = new Set();\n            /** @type {Set<string>} */\n            const unloaded = new Set();\n\n            for (const moduleName of moduleNames) {\n                const { deps, ignoreMissingDeps } = this.factories.get(moduleName);\n\n                dependencyGraph[moduleName] = deps;\n\n                if (ignoreMissingDeps) {\n                    continue;\n                }\n\n                unloaded.add(moduleName);\n                for (const dep of deps) {\n                    if (!this.factories.has(dep)) {\n                        missing.add(dep);\n                    }\n                }\n            }\n\n            const cycle = findCycle(moduleNames, new Set());\n            const errors = {};\n            if (cycle) {\n                errors.cycle = cycle;\n            }\n            if (this.failed.size) {\n                errors.failed = this.failed;\n            }\n            if (missing.size) {\n                errors.missing = missing;\n            }\n            if (unloaded.size) {\n                errors.unloaded = unloaded;\n            }\n            return errors;\n        }\n\n        /** @type {OdooModuleLoader[\"findJob\"]} */\n        findJob() {\n            for (const job of this.jobs) {\n                if (this.factories.get(job).deps.every((dep) => this.modules.has(dep))) {\n                    return job;\n                }\n            }\n            return null;\n        }\n\n        /** @type {OdooModuleLoader[\"reportErrors\"]} */\n        async reportErrors(errors) {\n            if (!Object.keys(errors).length) {\n                return;\n            }\n\n            if (errors.failed) {\n                console.error(\"The following modules failed to load because of an error:\", [\n                    ...errors.failed,\n                ]);\n            }\n            if (errors.missing) {\n                console.error(\n                    \"The following modules are needed by other modules but have not been defined, they may not be present in the correct asset bundle:\",\n                    [...errors.missing]\n                );\n            }\n            if (errors.cycle) {\n                console.error(\n                    \"The following modules could not be loaded because they form a dependency cycle:\",\n                    errors.cycle\n                );\n            }\n            if (errors.unloaded) {\n                console.error(\n                    \"The following modules could not be loaded because they have unmet dependencies, this is a secondary error which is likely caused by one of the above problems:\",\n                    [...errors.unloaded]\n                );\n            }\n\n            const document = this.root?.ownerDocument || globalThis.document;\n            if (document.readyState === \"loading\") {\n                await new Promise((resolve) =>\n                    document.addEventListener(\"DOMContentLoaded\", resolve)\n                );\n            }\n\n            const style = document.createElement(\"style\");\n            style.className = \"o_module_error_banner\";\n            style.textContent = `\n                body::before {\n                    font-weight: bold;\n                    content: \"An error occurred while loading javascript modules, you may find more information in the devtools console\";\n                    position: fixed;\n                    left: 0;\n                    bottom: 0;\n                    z-index: 100000000000;\n                    background-color: #C00;\n                    color: #DDD;\n                }\n            `;\n            document.head.appendChild(style);\n        }\n\n        /** @type {OdooModuleLoader[\"startModules\"]} */\n        startModules() {\n            let job;\n            while ((job = this.findJob())) {\n                this.startModule(job);\n            }\n        }\n\n        /** @type {OdooModuleLoader[\"startModule\"]} */\n        startModule(name) {\n            /** @type {(dependency: string) => OdooModule} */\n            const require = (dependency) => this.modules.get(dependency);\n            this.jobs.delete(name);\n            const factory = this.factories.get(name);\n            /** @type {OdooModule | null} */\n            let module = null;\n            try {\n                module = factory.fn(require);\n            } catch (error) {\n                this.failed.add(name);\n                throw new Error(`Error while loading \"${name}\":\\n${error}`);\n            }\n            this.modules.set(name, module);\n            this.bus.dispatchEvent(\n                new CustomEvent(\"module-started\", {\n                    detail: { moduleName: name, module },\n                })\n            );\n            return module;\n        }\n    }\n\n    if (odoo.debug && !new URLSearchParams(location.search).has(\"debug\")) {\n        // remove debug mode if not explicitely set in url\n        odoo.debug = \"\";\n    }\n\n    const loader = new ModuleLoader();\n    odoo.define = loader.define.bind(loader);\n    odoo.loader = loader;\n})((globalThis.odoo ||= {}));\n", "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", "(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                if (current instanceof RootFiber) {\r\n                    // it is possible that this fiber is a fiber that crashed while being\r\n                    // mounted, so the mounted list is possibly corrupted. We restore it to\r\n                    // its normal initial state (which is empty list or a list with a mount\r\n                    // fiber.\r\n                    current.mounted = current instanceof MountFiber ? [current] : [];\r\n                }\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            let mountedFibers = this.mounted;\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                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                // if mountedFibers is not empty, this means that a crash occured while\r\n                // calling the mounted hooks of some component. So, there may still be\r\n                // some component that have been mounted, but for which the mounted hooks\r\n                // have not been called. Here, we remove the willUnmount hooks for these\r\n                // specific component to prevent a worse situation (willUnmount being\r\n                // called even though mounted has not been called)\r\n                for (let fiber of mountedFibers) {\r\n                    fiber.node.willUnmount = [];\r\n                }\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 saveCurrent() {\r\n        let n = currentNode;\r\n        return () => {\r\n            currentNode = n;\r\n        };\r\n    }\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            this.customDirectives = config.customDirectives || {};\r\n            this.runtimeUtils = { ...helpers, __globals__: config.globalValues || {} };\r\n            this.hasGlobalValues = Boolean(config.globalValues && Object.keys(config.globalValues).length);\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, this.runtimeUtils);\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,__globals__\".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            translationCtx: parentCtx.translationCtx,\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            if (options.hasGlobalValues) {\r\n                this.helpers.add(\"__globals__\");\r\n            }\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                translationCtx: \"\",\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, translationCtx) {\r\n            const match = translationRE.exec(str);\r\n            return match[1] + this.translateFn(match[2], translationCtx) + 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 /* TTranslationContext */:\r\n                    return this.compileTTranslationContext(ast, ctx);\r\n                case 18 /* 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, ctx.translationCtx);\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            var _a;\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                    const attrTranslationCtx = ((_a = ast.attrsTranslationCtx) === null || _a === void 0 ? void 0 : _a[key]) || ctx.translationCtx;\r\n                    attrs[key] = this.translateFn(ast.attrs[key], attrTranslationCtx);\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, ctx.translationCtx) : 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, attrsTranslationCtx, translationCtx) {\r\n            if (name.endsWith(\".translate\")) {\r\n                const attrTranslationCtx = (attrsTranslationCtx === null || attrsTranslationCtx === void 0 ? void 0 : attrsTranslationCtx[name]) || translationCtx;\r\n                value = toStringExpression(this.translateFn(value, attrTranslationCtx));\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: ${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, attrsTranslationCtx, translationCtx) {\r\n            return Object.entries(obj).map(([k, v]) => this.formatProp(k, v, attrsTranslationCtx, translationCtx));\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\r\n                ? this.formatPropObject(ast.props, ast.propsTranslationCtx, ctx.translationCtx)\r\n                : [];\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, ast.slots[slotName].attrsTranslationCtx, ctx.translationCtx));\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 attrs = { ...ast.attrs };\r\n            const dynProps = attrs[\"t-props\"];\r\n            delete attrs[\"t-props\"];\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\r\n                ? this.formatPropObject(attrs, ast.attrsTranslationCtx, ctx.translationCtx)\r\n                : [];\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        compileTTranslationContext(ast, ctx) {\r\n            if (ast.content) {\r\n                return this.compileAST(ast.content, Object.assign({}, ctx, { translationCtx: ast.translationCtx }));\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, customDir) {\r\n        const ctx = {\r\n            inPreTag: false,\r\n            customDirectives: customDir,\r\n        };\r\n        if (typeof xml === \"string\") {\r\n            const elem = parseXML(`<t>${xml}</t>`).firstChild;\r\n            return _parse(elem, ctx);\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), ctx);\r\n            cache.set(xml, ast);\r\n        }\r\n        return ast;\r\n    }\r\n    function _parse(xml, ctx) {\r\n        normalizeXML(xml);\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 (parseTCustom(node, ctx) ||\r\n            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            parseTTranslationContext(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    function parseTCustom(node, ctx) {\r\n        if (!ctx.customDirectives) {\r\n            return null;\r\n        }\r\n        const nodeAttrsNames = node.getAttributeNames();\r\n        for (let attr of nodeAttrsNames) {\r\n            if (attr === \"t-custom\" || attr === \"t-custom-\") {\r\n                throw new OwlError(\"Missing custom directive name with t-custom directive\");\r\n            }\r\n            if (attr.startsWith(\"t-custom-\")) {\r\n                const directiveName = attr.split(\".\")[0].slice(9);\r\n                const customDirective = ctx.customDirectives[directiveName];\r\n                if (!customDirective) {\r\n                    throw new OwlError(`Custom directive \"${directiveName}\" is not defined`);\r\n                }\r\n                const value = node.getAttribute(attr);\r\n                const modifiers = attr.split(\".\").slice(1);\r\n                node.removeAttribute(attr);\r\n                try {\r\n                    customDirective(node, value, modifiers);\r\n                }\r\n                catch (error) {\r\n                    throw new OwlError(`Custom directive \"${directiveName}\" throw the following error: ${error}`);\r\n                }\r\n                return parseNode(node, ctx);\r\n            }\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 attrsTranslationCtx = 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.startsWith(\"t-translation-context-\")) {\r\n                const attrName = attr.slice(22);\r\n                attrsTranslationCtx = attrsTranslationCtx || {};\r\n                attrsTranslationCtx[attrName] = 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            attrsTranslationCtx,\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: {\r\n                        default: {\r\n                            content: tcall,\r\n                            scope: null,\r\n                            on: null,\r\n                            attrs: null,\r\n                            attrsTranslationCtx: null,\r\n                        },\r\n                    },\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        let propsTranslationCtx = null;\r\n        for (let name of node.getAttributeNames()) {\r\n            const value = node.getAttribute(name);\r\n            if (name.startsWith(\"t-translation-context-\")) {\r\n                const attrName = name.slice(22);\r\n                propsTranslationCtx = propsTranslationCtx || {};\r\n                propsTranslationCtx[attrName] = value;\r\n            }\r\n            else 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 attrsTranslationCtx = 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-translation-context-\")) {\r\n                        const attrName = attributeName.slice(22);\r\n                        attrsTranslationCtx = attrsTranslationCtx || {};\r\n                        attrsTranslationCtx[attrName] = value;\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, attrsTranslationCtx, 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 = {\r\n                    content: defaultContent,\r\n                    on,\r\n                    attrs: null,\r\n                    attrsTranslationCtx: null,\r\n                    scope: defaultSlotScope,\r\n                };\r\n            }\r\n        }\r\n        return {\r\n            type: 11 /* TComponent */,\r\n            name,\r\n            isDynamic,\r\n            dynamicProps,\r\n            props,\r\n            propsTranslationCtx,\r\n            slots,\r\n            on,\r\n        };\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 attrsTranslationCtx = 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 if (attributeName.startsWith(\"t-translation-context-\")) {\r\n                const attrName = attributeName.slice(22);\r\n                attrsTranslationCtx = attrsTranslationCtx || {};\r\n                attrsTranslationCtx[attrName] = 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            attrsTranslationCtx,\r\n            on,\r\n            defaultContent: parseChildNodes(node, ctx),\r\n        };\r\n    }\r\n    // -----------------------------------------------------------------------------\r\n    // Translation\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    // Translation Context\r\n    // -----------------------------------------------------------------------------\r\n    function parseTTranslationContext(node, ctx) {\r\n        const translationCtx = node.getAttribute(\"t-translation-context\");\r\n        if (!translationCtx) {\r\n            return null;\r\n        }\r\n        node.removeAttribute(\"t-translation-context\");\r\n        return {\r\n            type: 17 /* TTranslationContext */,\r\n            content: parseNode(node, ctx),\r\n            translationCtx,\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: 18 /* 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        hasGlobalValues: false,\r\n    }) {\r\n        // parsing\r\n        const ast = parse(template, options.customDirectives);\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.6.1\";\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.processing = false;\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            if (this.processing) {\r\n                return;\r\n            }\r\n            this.processing = true;\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            this.processing = false;\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                // at this point, the fiber should have been applied to the DOM, so we can\r\n                // remove it from the task list. If it is not the case, it means that there\r\n                // was an error and an error handler triggered a new rendering that recycled\r\n                // the fiber, so in that case, we actually want to keep the fiber around,\r\n                // otherwise it will just be ignored.\r\n                if (fiber.appliedToDom) {\r\n                    this.tasks.delete(fiber);\r\n                }\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 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(`Owl is running in 'dev' mode.`);\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 restore = saveCurrent();\r\n            const node = this.makeNode(Root, props);\r\n            restore();\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            customDirectives: this.customDirectives,\r\n            hasGlobalValues: this.hasGlobalValues,\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 = '2025-03-05T08:37:58.580Z';\r\n    __info__.hash = '2b5cea9';\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", "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", "export const session = odoo.__session_info__ || {};\ndelete odoo.__session_info__;\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 { Component, onWillStart, whenReady, xml } from \"@odoo/owl\";\nimport { session } from \"@web/session\";\nimport { registry } from \"./registry\";\n\n/**\n * @typedef {{\n *  cssLibs: string[];\n *  jsLibs: string[];\n * }} BundleFileNames\n */\n\nconst computeCacheMap = () => {\n    for (const script of document.head.querySelectorAll(\"script[src]\")) {\n        cacheMap.set(script.src, Promise.resolve());\n    }\n    for (const link of document.head.querySelectorAll(\"link[rel=stylesheet][href]\")) {\n        cacheMap.set(link.href, Promise.resolve());\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/** @type {Map<string, Promise<BundleFileNames | void>>} */\nconst cacheMap = new Map();\n\nwhenReady(computeCacheMap);\n\n/** @type {typeof assets[\"getBundle\"]} */\nexport function getBundle() {\n    return assets.getBundle(...arguments);\n}\n\n/** @type {typeof assets[\"loadBundle\"]} */\nexport function loadBundle() {\n    return assets.loadBundle(...arguments);\n}\n\n/** @type {typeof assets[\"loadJS\"]} */\nexport function loadJS() {\n    return assets.loadJS(...arguments);\n}\n\n/** @type {typeof assets[\"loadCSS\"]} */\nexport function loadCSS() {\n    return assets.loadCSS(...arguments);\n}\n\nexport class AssetsLoadingError extends Error {}\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\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    /**\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<BundleFileNames>}\n     */\n    getBundle(bundleName) {\n        if (cacheMap.has(bundleName)) {\n            return cacheMap.get(bundleName);\n        }\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 = fetch(url)\n            .then(async (response) => {\n                const cssLibs = [];\n                const jsLibs = [];\n                if (!response.bodyUsed) {\n                    const result = await response.json();\n                    for (const { src, type } of Object.values(result)) {\n                        if (type === \"link\" && src) {\n                            cssLibs.push(src);\n                        } else if (type === \"script\" && src) {\n                            jsLibs.push(src);\n                        }\n                    }\n                }\n                return { cssLibs, jsLibs };\n            })\n            .catch((reason) => {\n                cacheMap.delete(bundleName);\n                throw reason;\n            });\n        cacheMap.set(bundleName, promise);\n        return promise;\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<void[]>}\n     */\n    loadBundle(bundleName) {\n        if (typeof bundleName !== \"string\") {\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        return getBundle(bundleName).then(({ cssLibs, jsLibs }) =>\n            Promise.all([...cssLibs.map(loadCSS), ...jsLibs.map(loadJS)])\n        );\n    },\n\n    /**\n     * Loads the given url as a stylesheet.\n     *\n     * @param {string} url the url of the stylesheet\n     * @param {number} [retryCount]\n     * @returns {Promise<void>} resolved when the stylesheet has been loaded\n     */\n    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            onLoadAndError(linkEl, resolve, async () => {\n                cacheMap.delete(url);\n                if (retryCount < assets.retries.count) {\n                    const delay = assets.retries.delay + assets.retries.extraDelay * retryCount;\n                    await new Promise((res) => setTimeout(res, delay));\n                    linkEl.remove();\n                    loadCSS(url, retryCount + 1)\n                        .then(resolve)\n                        .catch((reason) => {\n                            cacheMap.delete(url);\n                            reject(reason);\n                        });\n                } else {\n                    reject(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     * Loads the given url inside a script tag.\n     *\n     * @param {string} url the url of the script\n     * @returns {Promise<void>} resolved when the script has been loaded\n     */\n    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", "import { Deferred } from \"@web/core/utils/concurrency\";\nimport { useAutofocus, useForwardRefToParent, useService } from \"@web/core/utils/hooks\";\nimport { isScrollableY, scrollTo } from \"@web/core/utils/scrolling\";\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        this.listRef = useRef(\"sourcesList\");\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}_${\n            source.isLoading ? \"loading\" : optionIndex\n        }`;\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    get activeOption() {\n        const [sourceIndex, optionIndex] = this.state.activeSourceOption;\n        return this.sources[sourceIndex].options[optionIndex];\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\n    selectOption(option, params = {}) {\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.trim());\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.activeOption);\n                break;\n            case \"escape\":\n                if (!this.isOpened) {\n                    return;\n                }\n                this.cancel();\n                break;\n            case \"tab\":\n            case \"shift+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.activeOption);\n                }\n                this.close();\n                return;\n            case \"arrowup\":\n                this.navigate(-1);\n                if (!this.isOpened) {\n                    this.open(true);\n                }\n                this.scroll();\n                break;\n            case \"arrowdown\":\n                this.navigate(+1);\n                if (!this.isOpened) {\n                    this.open(true);\n                }\n                this.scroll();\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(option) {\n        this.selectOption(option);\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    scroll() {\n        if (!this.activeSourceOptionId) {\n            return;\n        }\n        if (isScrollableY(this.listRef.el)) {\n            scrollTo(this.listRef.el.querySelector(`#${this.activeSourceOptionId}`));\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;\nlet localStorage;\ntry {\n    sessionStorage = window.sessionStorage;\n    localStorage = window.localStorage;\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", "/**\n * Utils to make use of document.cookie\n * https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies\n * As recommended, storage should not be done by the cookie\n * but with localStorage/sessionStorage\n */\n\nconst COOKIE_TTL = 24 * 60 * 60 * 365;\n\nexport const cookie = {\n    get _cookieMonster() {\n        return document.cookie;\n    },\n    set _cookieMonster(value) {\n        document.cookie = value;\n    },\n    get(str) {\n        const parts = this._cookieMonster.split(\"; \");\n        for (const part of parts) {\n            const [key, value] = part.split(/=(.*)/);\n            if (key === str) {\n                return value || \"\";\n            }\n        }\n    },\n    set(key, value, ttl = COOKIE_TTL) {\n        let fullCookie = [];\n        if (value !== undefined) {\n            fullCookie.push(`${key}=${value}`);\n        }\n        fullCookie = fullCookie.concat([\"path=/\", `max-age=${ttl}`]);\n        this._cookieMonster = fullCookie.join(\"; \");\n    },\n    delete(key) {\n        this.set(key, \"kill\", 0);\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 the user navigates the history using the back/forward button, some browsers (Safari iOS and\n * Safari MacOS) can restore the page using the `bfcache` (especially when we come back from an\n * external website). Unfortunately, Odoo wasn't designed to be compatible with this cache, which\n * leads to inconsistencies. When the `bfcache` is used to restore a page, we reload the current\n * page, to be sure that all the elements have been rendered correctly.\n */\nbrowser.addEventListener(\"pageshow\", (ev) => {\n    if (ev.persisted) {\n        browser.clearTimeout(pushTimeout);\n        routerBus.trigger(\"ROUTE_CHANGE\");\n    }\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 { registry } from \"../registry\";\n\nexport const titleService = {\n    start() {\n        const titleCounters = {};\n        const titleParts = {};\n\n        function getParts() {\n            return Object.assign({}, titleParts);\n        }\n\n        function setCounters(counters) {\n            for (const key in counters) {\n                const val = counters[key];\n                if (!val) {\n                    delete titleCounters[key];\n                } else {\n                    titleCounters[key] = val;\n                }\n            }\n            updateTitle();\n        }\n\n        function setParts(parts) {\n            for (const key in parts) {\n                const val = parts[key];\n                if (!val) {\n                    delete titleParts[key];\n                } else {\n                    titleParts[key] = val;\n                }\n            }\n            updateTitle();\n        }\n\n        function updateTitle() {\n            const counter = Object.values(titleCounters).reduce((acc, count) => acc + count, 0);\n            const name = Object.values(titleParts).join(\" - \") || \"Odoo\";\n            if (!counter) {\n                document.title = name;\n            } else {\n                document.title = `(${counter}) ${name}`;\n            }\n        }\n\n        return {\n            /**\n             * @returns {string}\n             */\n            get current() {\n                return document.title;\n            },\n            getParts,\n            setCounters,\n            setParts,\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"title\", titleService);\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 defaultCssColor = this.props.selectedColor\n                ? this.props.selectedColor\n                : this.props.defaultColor;\n            const rgba = convertCSSColorToRgba(defaultCssColor);\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            const newSelectedColor = newProps.selectedColor\n                ? newProps.selectedColor\n                : newProps.defaultColor;\n            this.setSelectedColor(newSelectedColor);\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 { registry } from \"@web/core/registry\";\n\nconst commandCategoryRegistry = registry.category(\"command_categories\");\ncommandCategoryRegistry\n    .add(\"app\", {}, { sequence: 10 })\n    .add(\"smart_action\", {}, { sequence: 15 })\n    .add(\"actions\", {}, { sequence: 30 })\n    .add(\"default\", {}, { sequence: 50 })\n    .add(\"view_switcher\", {}, { sequence: 100 })\n    .add(\"debug\", {}, { sequence: 110 })\n    .add(\"disabled\", {});\n", "import { useService } from \"@web/core/utils/hooks\";\n\nimport { useEffect } from \"@odoo/owl\";\n\n/**\n * @typedef {import(\"./command_service\").CommandOptions} CommandOptions\n */\n\n/**\n * This hook will subscribe/unsubscribe the given subscription\n * when the caller component will mount/unmount.\n *\n * @param {string} name\n * @param {()=>(void | import(\"@web/core/commands/command_palette\").CommandPaletteConfig)} action\n * @param {CommandOptions} [options]\n */\nexport function useCommand(name, action, options = {}) {\n    const commandService = useService(\"command\");\n    useEffect(\n        () => commandService.add(name, action, options),\n        () => []\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", "import { registry } from \"@web/core/registry\";\nimport { CommandPalette } from \"./command_palette\";\n\nimport { Component, EventBus } from \"@odoo/owl\";\n\n/**\n * @typedef {import(\"./command_palette\").CommandPaletteConfig} CommandPaletteConfig\n * @typedef {import(\"../hotkeys/hotkey_service\").HotkeyOptions} HotkeyOptions\n */\n\n/**\n * @typedef {{\n *  name: string;\n *  action: ()=>(void | CommandPaletteConfig);\n *  category?: string;\n *  href?: string;\n * }} Command\n */\n\n/**\n * @typedef {{\n *  category?: string;\n *  isAvailable?: ()=>(boolean);\n *  global?: boolean;\n *  hotkey?: string;\n *  hotkeyOptions?: HotkeyOptions\n * }} CommandOptions\n */\n\n/**\n * @typedef {Command & CommandOptions & {\n *  removeHotkey?: ()=>void;\n * }} CommandRegistration\n */\n\nconst commandCategoryRegistry = registry.category(\"command_categories\");\nconst commandProviderRegistry = registry.category(\"command_provider\");\nconst commandSetupRegistry = registry.category(\"command_setup\");\n\nclass DefaultFooter extends Component {\n    static template = \"web.DefaultFooter\";\n    static props = {\n        switchNamespace: { type: Function },\n    };\n    setup() {\n        this.elements = commandSetupRegistry\n            .getEntries()\n            .map((el) => ({ namespace: el[0], name: el[1].name }))\n            .filter((el) => el.name);\n    }\n\n    onClick(namespace) {\n        this.props.switchNamespace(namespace);\n    }\n}\n\nexport const commandService = {\n    dependencies: [\"dialog\", \"hotkey\", \"ui\"],\n    start(env, { dialog, hotkey: hotkeyService, ui }) {\n        /** @type {Map<CommandRegistration>} */\n        const registeredCommands = new Map();\n        let nextToken = 0;\n        let isPaletteOpened = false;\n        const bus = new EventBus();\n\n        hotkeyService.add(\"control+k\", openMainPalette, {\n            bypassEditableProtection: true,\n            global: true,\n        });\n\n        /**\n         * @param {CommandPaletteConfig} config command palette config merged with default config\n         * @param {Function} onClose called when the command palette is closed\n         * @returns the actual command palette config if the command palette is already open\n         */\n        function openMainPalette(config = {}, onClose) {\n            const configByNamespace = {};\n            for (const provider of commandProviderRegistry.getAll()) {\n                const namespace = provider.namespace || \"default\";\n                if (!configByNamespace[namespace]) {\n                    configByNamespace[namespace] = {\n                        categories: [],\n                        categoryNames: {},\n                    };\n                }\n            }\n\n            for (const [category, el] of commandCategoryRegistry.getEntries()) {\n                const namespace = el.namespace || \"default\";\n                const name = el.name;\n                if (namespace in configByNamespace) {\n                    configByNamespace[namespace].categories.push(category);\n                    configByNamespace[namespace].categoryNames[category] = name;\n                }\n            }\n\n            for (const [\n                namespace,\n                { emptyMessage, debounceDelay, placeholder },\n            ] of commandSetupRegistry.getEntries()) {\n                if (namespace in configByNamespace) {\n                    if (emptyMessage) {\n                        configByNamespace[namespace].emptyMessage = emptyMessage;\n                    }\n                    if (debounceDelay !== undefined) {\n                        configByNamespace[namespace].debounceDelay = debounceDelay;\n                    }\n                    if (placeholder) {\n                        configByNamespace[namespace].placeholder = placeholder;\n                    }\n                }\n            }\n\n            config = Object.assign(\n                {\n                    configByNamespace,\n                    FooterComponent: DefaultFooter,\n                    providers: commandProviderRegistry.getAll(),\n                },\n                config\n            );\n            return openPalette(config, onClose);\n        }\n\n        /**\n         * @param {CommandPaletteConfig} config\n         * @param {Function} onClose called when the command palette is closed\n         */\n        function openPalette(config, onClose) {\n            if (isPaletteOpened) {\n                bus.trigger(\"SET-CONFIG\", config);\n                return;\n            }\n\n            // Open Command Palette dialog\n            isPaletteOpened = true;\n            dialog.add(\n                CommandPalette,\n                {\n                    config,\n                    bus,\n                },\n                {\n                    onClose: () => {\n                        isPaletteOpened = false;\n                        if (onClose) {\n                            onClose();\n                        }\n                    },\n                }\n            );\n        }\n\n        /**\n         * @param {Command} command\n         * @param {CommandOptions} options\n         * @returns {number} token\n         */\n        function registerCommand(command, options) {\n            if (!command.name || !command.action || typeof command.action !== \"function\") {\n                throw new Error(\"A Command must have a name and an action function.\");\n            }\n            const registration = Object.assign({}, command, options);\n            if (registration.identifier) {\n                const commandsArray = Array.from(registeredCommands.values());\n                const sameName = commandsArray.find((com) => com.name === registration.name);\n                if (sameName) {\n                    if (registration.identifier !== sameName.identifier) {\n                        registration.name += ` (${registration.identifier})`;\n                        sameName.name += ` (${sameName.identifier})`;\n                    }\n                } else {\n                    const sameFullName = commandsArray.find(\n                        (com) => com.name === registration.name + `(${registration.identifier})`\n                    );\n                    if (sameFullName) {\n                        registration.name += ` (${registration.identifier})`;\n                    }\n                }\n            }\n            if (registration.hotkey) {\n                const action = async () => {\n                    const commandService = env.services.command;\n                    const config = await command.action();\n                    if (!isPaletteOpened && config) {\n                        commandService.openPalette(config);\n                    }\n                };\n                registration.removeHotkey = hotkeyService.add(registration.hotkey, action, {\n                    ...options.hotkeyOptions,\n                    global: registration.global,\n                    isAvailable: (...args) => {\n                        let available = true;\n                        if (registration.isAvailable) {\n                            available = registration.isAvailable(...args);\n                        }\n                        if (available && options.hotkeyOptions?.isAvailable) {\n                            available = options.hotkeyOptions?.isAvailable(...args);\n                        }\n                        return available;\n                    },\n                });\n            }\n\n            const token = nextToken++;\n            registeredCommands.set(token, registration);\n            if (!options.activeElement) {\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 activate\n                // element of the subscription.\n                Promise.resolve().then(() => {\n                    registration.activeElement = ui.activeElement;\n                });\n            }\n\n            return token;\n        }\n\n        /**\n         * Unsubscribes the token corresponding subscription.\n         *\n         * @param {number} token\n         */\n        function unregisterCommand(token) {\n            const cmd = registeredCommands.get(token);\n            if (cmd && cmd.removeHotkey) {\n                cmd.removeHotkey();\n            }\n            registeredCommands.delete(token);\n        }\n\n        return {\n            /**\n             * @param {string} name\n             * @param {()=>(void | CommandPaletteConfig)} action\n             * @param {CommandOptions} [options]\n             * @returns {() => void}\n             */\n            add(name, action, options = {}) {\n                const token = registerCommand({ name, action }, options);\n                return () => {\n                    unregisterCommand(token);\n                };\n            },\n            /**\n             * @param {HTMLElement} activeElement\n             * @returns {Command[]}\n             */\n            getCommands(activeElement) {\n                return [...registeredCommands.values()].filter(\n                    (command) => command.activeElement === activeElement || command.global\n                );\n            },\n            openMainPalette,\n            openPalette,\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"command\", commandService);\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 \"../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\"][\"prototype\"]} 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 | DateRange, unit: \"date\" | \"time\") => 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 {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 {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\n            /** @type {WeekItem[]} */\n            const lastWeeks = [];\n            let shouldAddLastWeek = false;\n\n            const dayItems = 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 < WEEKS_PER_MONTH; w++) {\n                    const weekDayItems = [];\n                    // Generate all days of the week\n                    for (let d = 0; d < DAYS_PER_WEEK; 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 === DAYS_PER_WEEK - 1) {\n                            startOfNextWeek = day.plus({ day: 1 });\n                        }\n                        if (w === WEEKS_PER_MONTH - 1) {\n                            shouldAddLastWeek ||= !dayItem.isOutOfRange;\n                        }\n                    }\n\n                    const weekItem = toWeekItem(weekDayItems);\n                    if (w === WEEKS_PER_MONTH - 1) {\n                        lastWeeks.push(weekItem);\n                    } else {\n                        weeks.push(weekItem);\n                    }\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            if (shouldAddLastWeek) {\n                // Add last empty week item if the other month has an extra week\n                for (let i = 0; i < dayItems.length; i++) {\n                    dayItems[i].weeks.push(lastWeeks[i]);\n                }\n            }\n\n            return dayItems;\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\nconst DAYS_PER_WEEK = 7;\nconst WEEKS_PER_MONTH = 6;\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 { dayCellClass, focusedDateIndex, isDateValid, range, showWeekNumbers } = this.props;\n        const { focusDate, 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: showWeekNumbers ?? !range,\n            isDateValid,\n            dayCellClass,\n        };\n\n        this.title = precision.getTitle(focusDate, getterParams);\n        this.items = precision.getItems(focusDate, getterParams);\n\n        this.selectedRange = [...this.values];\n        if (range && focusedDateIndex > 0 && (!this.values[1] || hoveredDate > this.values[0])) {\n            this.selectedRange[1] = hoveredDate;\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 {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: isInRange(this.state.hoveredDate, range),\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            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        }\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, \"time\");\n    }\n\n    /**\n     * @param {DateTime} value\n     * @param {number} valueIndex\n     * @param {\"date\" | \"time\"} unit\n     */\n    validateAndSelect(value, valueIndex, unit) {\n        if (!this.props.onSelect) {\n            // No onSelect handler\n            return false;\n        }\n\n        const result = [...this.values];\n        result[valueIndex] = value;\n\n        if (this.props.type === \"datetime\") {\n            // Adjusts result according to the current time values\n            const [hour, minute, second] = this.getTimeValues(valueIndex);\n            result[valueIndex] = result[valueIndex].set({ hour, minute, second });\n        }\n        if (!isInRange(result[valueIndex], [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], unit);\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, \"date\");\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\"][\"prototype\"]} 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                    const valueCopy = deepCopy(pickerProps.value);\n                    if (areDatesEqual(lastAppliedValue, valueCopy)) {\n                        return;\n                    }\n\n                    inputsChanged = ensureArray(pickerProps.value).map(() => false);\n\n                    hookParams.onApply?.(pickerProps.value);\n                    lastAppliedValue = valueCopy;\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                    lastAppliedValue = propsCopy.value;\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                 * @param {\"date\" | \"time\"} unit\n                 * @param {\"input\" | \"picker\"} source\n                 */\n                const updateValue = (value, unit, source) => {\n                    const previousValue = pickerProps.value;\n                    pickerProps.value = value;\n\n                    if (areDatesEqual(previousValue, pickerProps.value)) {\n                        return;\n                    }\n\n                    if (unit !== \"time\") {\n                        if (pickerProps.range && source === \"picker\") {\n                            if (\n                                pickerProps.focusedDateIndex === 0 ||\n                                (value[0] && value[1] && value[1] < value[0])\n                            ) {\n                                // If selecting either:\n                                // - the first value\n                                // - OR a second value before the first:\n                                // Then:\n                                // - Set the DATE (year + month + day) of all values\n                                // to the one that has been selected.\n                                const { year, month, day } = value[pickerProps.focusedDateIndex];\n                                for (let i = 0; i < value.length; i++) {\n                                    value[i] = value[i] && value[i].set({ year, month, day });\n                                }\n                                pickerProps.focusedDateIndex = 1;\n                            } else {\n                                // If selecting the second value after the first:\n                                // - simply toggle the focus index\n                                pickerProps.focusedDateIndex =\n                                    pickerProps.focusedDateIndex === 1 ? 0 : 1;\n                            }\n                        }\n                    }\n\n                    hookParams.onChange?.(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], \"date\", \"input\");\n                };\n\n                // Hook variables\n\n                /** @type {DateTimePickerProps} */\n                const rawPickerProps = {\n                    ...DateTimePicker.defaultProps,\n                    onSelect: (value, unit) => {\n                        value &&= markRaw(value);\n                        updateValue(value, unit, \"picker\");\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                /** @type {DateTimePickerProps[\"value\"] | null}*/\n                let lastAppliedValue = 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 { _t } from \"@web/core/l10n/translation\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { DebugMenuBasic } from \"@web/core/debug/debug_menu_basic\";\nimport { useCommand } from \"@web/core/commands/command_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useEnvDebugContext } from \"./debug_context\";\n\nexport class DebugMenu extends DebugMenuBasic {\n    static components = { Dropdown, DropdownItem };\n    static props = {};\n    setup() {\n        super.setup();\n        const debugContext = useEnvDebugContext();\n        this.command = useService(\"command\");\n        useCommand(\n            _t(\"Debug tools...\"),\n            async () => {\n                const items = await debugContext.getItems(this.env);\n                let index = 0;\n                const defaultCategories = items\n                    .filter((item) => item.type === \"separator\")\n                    .map(() => (index += 1));\n                const provider = {\n                    async provide() {\n                        const categories = [...defaultCategories];\n                        let category = categories.shift();\n                        const result = [];\n                        items.forEach((item) => {\n                            if (item.type === \"item\") {\n                                result.push({\n                                    name: item.description.toString(),\n                                    action: item.callback,\n                                    category,\n                                });\n                            } else if (item.type === \"separator\") {\n                                category = categories.shift();\n                            }\n                        });\n                        return result;\n                    },\n                };\n                const configByNamespace = {\n                    default: {\n                        categories: defaultCategories,\n                        emptyMessage: _t(\"No debug command found\"),\n                        placeholder: _t(\"Choose a debug command...\"),\n                    },\n                };\n                const commandPaletteConfig = {\n                    configByNamespace,\n                    providers: [provider],\n                };\n                return commandPaletteConfig;\n            },\n            {\n                category: \"debug\",\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\nexport function becomeSuperuser({ env }) {\n    const becomeSuperuserURL = browser.location.origin + \"/web/become\";\n    if (!user.isAdmin) {\n        return false;\n    }\n    return {\n        type: \"item\",\n        description: _t(\"Become Superuser\"),\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 { session } from '@web/session';\nimport { patch } from '@web/core/utils/patch';\n\nimport { Dialog } from '@web/core/dialog/dialog';\n\npatch(Dialog.prototype, {\n\tsetup() {\n        super.setup();\n        this.data.size = (\n    \t\tsession.dialog_size !== 'maximize' ? this.props.size : 'fs'\n        );\n        this.data.initalSize = this.props?.size || 'lg';\n    },\n    onClickDialogSizeToggle() {\n        this.data.size = this.data.size === 'fs' ? this.data.initalSize : 'fs';\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    const hierarchyOperators = fieldDef.allow_hierachy_operators ? [\"child_of\", \"parent_of\"] : [];\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                ...hierarchyOperators,\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                \"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        const rootNode = target.getRootNode();\n        if (rootNode instanceof ShadowRoot) {\n            target = rootNode.host;\n        }\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        slots: { type: Object, optional: true },\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\";\nimport { useEffect, useExternalListener } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @param {Ref} targetRef - Element on which to place the dropzone.\n * @param {Class} dropzoneComponent - Class used to instantiate the dropzone component.\n * @param {Object} dropzoneComponentProps - Props given to the instantiated dropzone component.\n * @param {function} isDropzoneEnabled - Function that determines whether the dropzone should be enabled.\n */\nexport function useCustomDropzone(targetRef, dropzoneComponent, dropzoneComponentProps, isDropzoneEnabled = () => true) {\n    const overlayService = useService(\"overlay\");\n    const uiService = useService(\"ui\");\n\n    let dragCount = 0;\n    let hasTarget = false;\n    let removeDropzone = false;\n\n    useExternalListener(document, \"dragenter\", onDragEnter, { capture: true });\n    useExternalListener(document, \"dragleave\", onDragLeave, { capture: true });\n    // Prevents the browser to open or download the file when it is dropped\n    // outside of the dropzone.\n    useExternalListener(window, \"dragover\", (ev) => {\n        if (ev.dataTransfer && ev.dataTransfer.types.includes(\"Files\")) {\n            ev.preventDefault();\n        }\n    });\n    useExternalListener(\n        window,\n        \"drop\",\n        (ev) => {\n            if (ev.dataTransfer && ev.dataTransfer.types.includes(\"Files\")) {\n                ev.preventDefault();\n            }\n            dragCount = 0;\n            updateDropzone();\n        },\n        { capture: true }\n    );\n\n    function updateDropzone() {\n        const hasDropzone = !!removeDropzone;\n        const isTargetInActiveElement = uiService.activeElement.contains(targetRef.el);\n        const shouldDisplayDropzone = dragCount && hasTarget && isTargetInActiveElement && isDropzoneEnabled();\n\n        if (shouldDisplayDropzone && !hasDropzone) {\n            removeDropzone = overlayService.add(dropzoneComponent, {\n                ref: targetRef,\n                ...dropzoneComponentProps\n            });\n        }\n        if (!shouldDisplayDropzone && hasDropzone) {\n            removeDropzone();\n            removeDropzone = false;\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\n/**\n * @param {Ref} targetRef - Element on which to place the dropzone.\n * @param {function} onDrop - Callback function called when the user drops a file on the dropzone.\n * @param {string} extraClass - Classes that will be added to the standard `Dropzone` component.\n * @param {function} isDropzoneEnabled - Function that determines whether the dropzone should be enabled.\n */\nexport function useDropzone(targetRef, onDrop, extraClass, isDropzoneEnabled = () => true) {\n    const dropzoneComponent = Dropzone;\n    const dropzoneComponentProps = { extraClass, onDrop };\n    useCustomDropzone(targetRef, dropzoneComponent, dropzoneComponentProps, isDropzoneEnabled);\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, {\n        ...newOptions,\n        animation: false,\n        popoverClass: \"border-secondary\",\n    });\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_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    keyboardNavigated = false;\n\n    setup() {\n        this.gridRef = useRef(\"emoji-grid\");\n        this.navbarRef = useRef(\"navbar\");\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.recentCategory = {\n                name: \"Frequently used\",\n                displayName: _t(\"Frequently used\"),\n                title: \"\ud83d\udd53\",\n                sortId: 0,\n            };\n            this.state.categoryId = this.recentEmojis.length\n                ? this.recentCategory.sortId\n                : this.categories[0].sortId;\n        });\n        onMounted(() => {\n            this.navbarResizeObserver = new ResizeObserver(() => this.adaptNavbar());\n            this.navbarResizeObserver.observe(this.navbarRef.el);\n            this.adaptNavbar();\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            () => this.updateEmojiPickerRepr(),\n            () => [this.state.categoryId, this.state.searchTerm]\n        );\n        useEffect(\n            (el) => {\n                const gridEl = this.gridRef?.el;\n                const activeEl = gridEl?.querySelector(\".o-Emoji.o-active\");\n                if (\n                    gridEl &&\n                    activeEl &&\n                    this.keyboardNavigated &&\n                    !isElementVisible(activeEl, gridEl)\n                ) {\n                    activeEl.scrollIntoView({ block: \"center\", behavior: \"instant\" });\n                    this.keyboardNavigated = false;\n                }\n            },\n            () => [this.state.activeEmojiIndex, this.gridRef?.el]\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            this.navbarResizeObserver.disconnect();\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    adaptNavbar() {\n        const computedStyle = getComputedStyle(this.navbarRef.el);\n        const availableWidth =\n            this.navbarRef.el.getBoundingClientRect().width -\n            parseInt(computedStyle.paddingLeft) -\n            parseInt(computedStyle.marginLeft) -\n            parseInt(computedStyle.paddingLeft) -\n            parseInt(computedStyle.marginLeft);\n        const itemWidth = this.navbarRef.el.querySelector(\".o-Emoji\").getBoundingClientRect().width;\n        const gapWidth = parseInt(computedStyle.gap);\n        const maxAvailableNavbarItemAmountAtOnce = Math.floor(\n            availableWidth / (itemWidth + gapWidth)\n        );\n        const repr = [];\n        let panel = [];\n        const allCategories = this.getAllCategories();\n        for (const category of allCategories) {\n            if (\n                panel.length === maxAvailableNavbarItemAmountAtOnce - 1 &&\n                category !== allCategories.at(-1)\n            ) {\n                panel.push(\"next\");\n                repr.push(panel);\n                panel = [];\n                panel.push(\"previous\");\n            }\n            panel.push(category.sortId);\n        }\n        if (panel.length > 0) {\n            if (repr.length > 0) {\n                panel.push(\n                    ...[...Array(maxAvailableNavbarItemAmountAtOnce - panel.length)].map(\n                        (_, idx) => \"empty_\" + idx\n                    )\n                );\n            }\n            repr.push(panel);\n        }\n        this.state.emojiNavbarRepr = repr;\n    }\n\n    get currentNavbarPanel() {\n        if (!this.state.emojiNavbarRepr) {\n            return this.getAllCategories().map((c) => c.sortId);\n        }\n        if (this.state.categoryId === null || Number.isNaN(this.state.categoryId)) {\n            return this.state.emojiNavbarRepr[0];\n        }\n        return this.state.emojiNavbarRepr.find((panel) => panel.includes(this.state.categoryId));\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,\n                ...emoji.keywords,\n                ...emoji.emoticons,\n                ...emoji.shortcodes,\n            ]);\n        }\n        return recent.slice(0, 42);\n    }\n\n    onClick(ev) {\n        markEventHandled(ev, \"emoji.selectEmoji\");\n    }\n\n    onClickToNextCategories() {\n        const panelIndex = this.state.emojiNavbarRepr.findIndex((p) =>\n            p.includes(this.state.categoryId)\n        );\n        this.selectCategory(this.state.emojiNavbarRepr[panelIndex + 1][1]);\n    }\n\n    onClickToPreviousCategories() {\n        const panelIndex = this.state.emojiNavbarRepr.findIndex((p) =>\n            p.includes(this.state.categoryId)\n        );\n        this.selectCategory(this.state.emojiNavbarRepr[panelIndex - 1].at(-2));\n    }\n\n    /**\n     * Builds the representation of the emoji picker (a 2D matrix of emojis)\n     * from the current DOM state. This is necessary to handle keyboard\n     * navigation of the emoji picker.\n     */\n    updateEmojiPickerRepr() {\n        const emojiEls = Array.from(this.gridRef.el.querySelectorAll(\".o-Emoji\"));\n        const emojiRects = emojiEls.map((el) => el.getBoundingClientRect());\n        this.emojiMatrix = [];\n        for (const [index, pos] of emojiRects.entries()) {\n            const emojiIndex = emojiEls[index].dataset.index;\n            if (this.emojiMatrix.length === 0 || pos.top > emojiRects[index - 1].top) {\n                this.emojiMatrix.push([]);\n            }\n            this.emojiMatrix.at(-1).push(parseInt(emojiIndex));\n        }\n    }\n\n    handleNavigation(key) {\n        const currentIdx = this.state.activeEmojiIndex;\n        let currentRow = -1;\n        let currentCol = -1;\n        const rowIdx = this.emojiMatrix.findIndex((row) => row.includes(currentIdx));\n        if (rowIdx !== -1) {\n            currentRow = rowIdx;\n            currentCol = this.emojiMatrix[currentRow].indexOf(currentIdx);\n        }\n        let newIdx;\n        switch (key) {\n            case \"ArrowDown\": {\n                const rowBelow = this.emojiMatrix[currentRow + 1];\n                const rowBelowBelow = this.emojiMatrix[currentRow + 2];\n                if (rowBelow?.length <= currentCol && rowBelowBelow?.length >= currentCol) {\n                    newIdx = rowBelowBelow?.[currentCol];\n                } else {\n                    newIdx = rowBelow?.[Math.min(currentCol, rowBelow.length - 1)];\n                }\n                break;\n            }\n            case \"ArrowUp\": {\n                const rowAbove = this.emojiMatrix[currentRow - 1];\n                const rowAboveAbove = this.emojiMatrix[currentRow - 2];\n                if (rowAbove?.length <= currentCol && rowAboveAbove?.length >= currentCol) {\n                    newIdx = rowAboveAbove?.[currentCol];\n                } else {\n                    newIdx = rowAbove?.[Math.min(currentCol, rowAbove.length - 1)];\n                }\n                break;\n            }\n            case \"ArrowRight\": {\n                const colRight = currentCol + 1;\n                if (colRight === this.emojiMatrix[currentRow].length) {\n                    const rowBelowRight = this.emojiMatrix[currentRow + 1];\n                    newIdx = rowBelowRight?.[0];\n                } else {\n                    newIdx = this.emojiMatrix[currentRow][colRight];\n                }\n                break;\n            }\n            case \"ArrowLeft\": {\n                const colLeft = currentCol - 1;\n                if (colLeft < 0) {\n                    const rowAboveLeft = this.emojiMatrix[currentRow - 1];\n                    newIdx = rowAboveLeft?.[rowAboveLeft.length - 1] ?? this.state.activeEmojiIndex;\n                } else {\n                    newIdx = this.emojiMatrix[currentRow][colLeft];\n                }\n                break;\n            }\n        }\n        this.state.activeEmojiIndex = newIdx ?? this.state.activeEmojiIndex;\n    }\n\n    onKeydown(ev) {\n        switch (ev.key) {\n            case \"ArrowDown\":\n            case \"ArrowUp\":\n            case \"ArrowRight\":\n            case \"ArrowLeft\":\n                this.handleNavigation(ev.key);\n                this.keyboardNavigated = true;\n                break;\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    getAllCategories() {\n        const res = [...this.categories];\n        if (this.recentEmojis.length > 0) {\n            res.unshift(this.recentCategory);\n        }\n        return res;\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 > 0) {\n            return fuzzyLookup(this.searchTerm, emojisToDisplay, (emoji) => [\n                emoji.name,\n                ...emoji.keywords,\n                ...emoji.emoticons,\n                ...emoji.shortcodes,\n            ]);\n        }\n        return emojisToDisplay;\n    }\n\n    getEmojisFromSearch() {\n        return [...this.recentEmojis, ...this.getEmojis()];\n    }\n\n    selectCategory(categoryId) {\n        this.searchTerm = \"\";\n        this.state.categoryId = categoryId;\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 + 10, coords.y + 10);\n        if (!res) {\n            return;\n        }\n        this.state.categoryId = parseInt(res.dataset.category);\n    }\n}\n\nfunction isElementVisible(el, holder) {\n    const offset = 20;\n    holder = holder || document.body;\n    const { top, bottom, height } = el.getBoundingClientRect();\n    let { top: holderTop, bottom: holderBottom } = holder.getBoundingClientRect();\n    holderTop += offset * 2; // section are position sticky top so emoji can be \"visible\" under section name. Overestimate to assume invisible.\n    holderBottom -= offset;\n    return top - offset <= holderTop ? holderTop - top <= height : bottom - holderBottom <= height;\n}\n", "import { loadBundle, loadJS } 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    } else if (!window.jQuery.fn.getScrollingElement) {\n        await loadJS(\"/web/static/src/legacy/js/libs/jquery.js\");\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 { session } from \"@web/session\";\nimport { user } from \"@web/core/user\";\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\n// -----------------------------------------------------------------------------\n// Frontend visitors errors\n// -----------------------------------------------------------------------------\n\n/**\n * We don't want to show tracebacks to non internal users. This handler swallows\n * all errors if we're not an internal user (except in debug or test mode).\n */\nexport function swallowAllVisitorErrors(env, error, originalError) {\n    if (!user.isInternalUser && !odoo.debug && !session.test_mode) {\n        return true;\n    }\n}\n\nif (user.isInternalUser === undefined) {\n    // Only warn about this while on the \"frontend\": the session info might\n    // apparently not be present in all Odoo screens at the moment... TODO ?\n    if (session.is_frontend) {\n        console.warn(\n            \"isInternalUser information is required for this handler to work. It must be available in the page.\"\n        );\n    }\n} else {\n    registry.category(\"error_handlers\").add(\"swallowAllVisitorErrors\", swallowAllVisitorErrors, { sequence: 0 });\n}\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            function shouldLogError() {\n                // Only log errors that are relevant business-wise, following the heuristics:\n                // Error.event and Error.traceback have been assigned\n                // in one of the two error event listeners below.\n                // If preventDefault was already executed on the event, don't log it.\n                return (\n                    uncaughtError.event &&\n                    !uncaughtError.event.defaultPrevented &&\n                    uncaughtError.traceback\n                );\n            }\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                    if (shouldLogError()) {\n                        uncaughtError.event.preventDefault();\n                        console.error(\n                            `@web/core/error_service: handler \"${name}\" failed with \"${\n                                e.cause || e\n                            }\" while trying to handle:\\n` + uncaughtError.traceback\n                        );\n                    }\n                    return;\n                }\n            }\n            if (shouldLogError()) {\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 instanceof Error) && errorsToIgnore.includes(message)) {\n                ev.preventDefault();\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    \"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) =>\n    class extends T {\n        access_token;\n        checksum;\n        extension;\n        filename;\n        id;\n        mimetype;\n        name;\n        /** @type {\"binary\"|\"url\"} */\n        type;\n        /** @type {string} */\n        tmpUrl;\n        /**\n         * This URL should not be used as the URL to serve the file. `urlRoute` should be used\n         * instead. The server will properly redirect to the correct URL when necessary.\n         *\n         * @type {string}\n         */\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.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                    if (document.activeElement) {\n                        document.activeElement.blur();\n                    }\n                    el.focus();\n                    setTimeout(() => 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, in the user's locale settings.\n *\n * @param {Date | luxon.DateTime} date\n * @returns {number}\n *  the ISO week number (1-53) of the Monday nearest to the locale's first day of the week\n */\nexport function getLocalWeekNumber(date) {\n    return getLocalYearAndWeek(date).week;\n}\n\n/**\n * Get the week year and week number of a given date, in the user's locale settings.\n *\n * @param {Date | luxon.DateTime} date\n * @returns {{ year: number, week: number }}\n *  the year the week is part of, and\n *  the ISO week number (1-53) of the Monday nearest to the locale's first day of the week\n */\nexport function getLocalYearAndWeek(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 { year: date.year, week: 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 { markup } from \"@odoo/owl\";\n\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { escape, 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\nconst Markup = markup().constructor;\n\n/**\n * Translates a term, or returns the term as it is if no translation can be\n * found.\n *\n * Extra positional arguments are inserted in place of %s placeholders.\n *\n * If the first extra argument is an object, the keys of that object are used to\n * map its entries to keyworded placeholders (%(kw_placeholder)s) for\n * replacement.\n *\n * If at least one of the extra arguments is a markup, the translation and\n * non-markup content are escaped, and the result is wrapped in a markup.\n *\n * @example\n * _t(\"Good morning\"); // \"Bonjour\"\n * _t(\"Good morning %s\", user.name); // \"Bonjour Marc\"\n * _t(\"Good morning %(newcomer)s, goodbye %(departer)s\", { newcomer: Marc, departer: Mitchel }); // Bonjour Marc, au revoir Mitchel\n * _t(\"I love %s\", markup(\"<blink>Minecraft</blink>\")); // Markup {\"J'adore <blink>Minecraft</blink>\"}\n *\n * @param {string} term\n * @returns {string|Markup|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 _safeSprintf(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 _safeSprintf(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\n/**\n * Same behavior as sprintf, but if any of the provided values is a markup,\n * escapes all non-markup content before performing the interpolation, then\n * wraps the result in a markup.\n *\n * @param {string} str The string with placeholders (%s) to insert values into.\n * @param  {...any} values Primitive values to insert in place of placeholders.\n * @returns {string|Markup}\n */\nfunction _safeSprintf(str, ...values) {\n    let hasMarkup;\n    if (values.length === 1 && Object.prototype.toString.call(values[0]) === \"[object Object]\") {\n        hasMarkup = Object.values(values[0]).some((v) => v instanceof Markup);\n    } else {\n        hasMarkup = values.some((v) => v instanceof Markup);\n    }\n    if (hasMarkup) {\n        return markup(sprintf(escape(str), ..._escapeNonMarkup(values)));\n    }\n    return sprintf(str, ...values);\n}\n\n/**\n * Go through each value to be passed to sprintf and escape anything that isn't\n * a markup.\n *\n * @param {any[]|[Object]} values Values for use with sprintf.\n * @returns {any[]|[Object]}\n */\nfunction _escapeNonMarkup(values) {\n    if (Object.prototype.toString.call(values[0]) === \"[object Object]\") {\n        const sanitized = {};\n        for (const [key, value] of Object.entries(values[0])) {\n            sanitized[key] = value instanceof Markup ? value : escape(value);\n        }\n        return [sanitized];\n    }\n    return values.map((x) => (x instanceof Markup ? x : escape(x)));\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 * Exceptions:\n *  - Serbian can be written in both Latin and Cyrillic scripts interchangeably,\n *  therefore its locale includes a special modifier to indicate which script to\n *  use. Example: sr-Latn \u2192 sr@latin\n *  - Tagalog/Filipino: The \"fil\" locale is replaced by \"tl\" for compatibility\n *  with the Python side (where the \"fil\" locale doesn't exist).\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        // new Intl.Locale(\"tl-PH\") produces fil-PH, which one might not expect\n        if (language === \"fil\") {\n            language = \"tl\";\n        }\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 { delay, Mutex } from \"@web/core/utils/concurrency\";\nimport { validate } from \"@odoo/owl\";\n\nconst macroSchema = {\n    name: { type: String, optional: true },\n    checkDelay: { type: Number, optional: true }, //Delay before checking if element is in DOM.\n    stepDelay: { type: Number, optional: true }, //Wait this delay between steps\n    timeout: { type: Number, optional: true },\n    steps: {\n        type: Array,\n        element: {\n            type: Object,\n            shape: {\n                action: { type: [Function, String], optional: true },\n                initialDelay: { type: Function, optional: true },\n                timeout: { type: Number, optional: true },\n                trigger: { type: [Function, String], optional: true },\n                value: { type: [String, Number], optional: true },\n            },\n            validate: (step) => {\n                return step.action || step.trigger;\n            },\n        },\n    },\n    onComplete: { type: Function, optional: true },\n    onStep: { type: Function, optional: true },\n    onError: { type: Function, optional: true },\n};\n\nconst mutex = new Mutex();\n\nclass MacroError extends Error {\n    constructor(type, message, options) {\n        super(message, options);\n        this.type = type;\n    }\n}\n\nexport async function waitForStable(target = document, timeout = 1000 / 16) {\n    return new Promise((resolve) => {\n        let observer;\n        let timer;\n        const mutationList = [];\n        function onMutation(mutations) {\n            mutationList.push(...(mutations || []));\n            clearTimeout(timer);\n            timer = setTimeout(() => {\n                observer.disconnect();\n                resolve(mutationList);\n            }, timeout);\n        }\n        observer = new MacroMutationObserver(onMutation);\n        observer.observe(target);\n        onMutation([]);\n    });\n}\n\nexport class Macro {\n    currentIndex = 0;\n    isComplete = false;\n    calledBack = false;\n    constructor(descr) {\n        try {\n            validate(descr, macroSchema);\n        } catch (error) {\n            throw new Error(\n                `Error in schema for Macro ${JSON.stringify(descr, null, 4)}\\n${error.message}`\n            );\n        }\n        Object.assign(this, descr);\n        this.name = this.name || \"anonymous\";\n        this.onComplete = this.onComplete || (() => {});\n        this.onStep = this.onStep || (() => {});\n        this.onError =\n            this.onError ||\n            ((e) => {\n                console.error(e);\n            });\n        this.stepElFound = new Array(this.steps.length).fill(false);\n        this.stepHasStarted = new Array(this.steps.length).fill(false);\n        this.observer = new MacroMutationObserver(() => this.debounceAdvance(\"mutation\"));\n    }\n\n    async start(target = document) {\n        this.observer.observe(target);\n        this.debounceAdvance(\"next\");\n    }\n\n    getDebounceDelay() {\n        let delay = Math.max(this.checkDelay ?? 750, 50);\n        // Called only once per step.\n        if (!this.stepHasStarted[this.currentIndex]) {\n            delay = this.currentIndex === 0 ? 0 : 50;\n            this.stepHasStarted[this.currentIndex] = true;\n            if (this.currentStep?.initialDelay) {\n                const initialDelay = parseFloat(this.currentStep.initialDelay());\n                delay = initialDelay >= 0 ? initialDelay : delay;\n            }\n        }\n        return delay;\n    }\n\n    async advance() {\n        if (this.isComplete) {\n            return;\n        }\n        if (this.currentStep.trigger) {\n            this.setTimer();\n        }\n        let proceedToAction = true;\n        if (this.currentStep.trigger) {\n            proceedToAction = this.findTrigger();\n        }\n        if (proceedToAction) {\n            this.onStep(this.currentElement, this.currentStep, this.currentIndex);\n            this.clearTimer();\n            const actionResult = await this.stepAction(this.currentElement);\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.stop();\n                }\n                this.debounceAdvance(\"next\");\n            }\n        }\n    }\n\n    /**\n     * Find the trigger and assess whether it can continue on performing the actions.\n     * @returns {boolean}\n     */\n    findTrigger() {\n        const { trigger } = this.currentStep;\n        if (this.isComplete) {\n            return;\n        }\n        try {\n            if (typeof trigger === \"function\") {\n                this.currentElement = trigger();\n            } else if (typeof trigger === \"string\") {\n                const triggerEl = document.querySelector(trigger);\n                this.currentElement = isVisible(triggerEl) && triggerEl;\n            } else {\n                throw new Error(`Trigger can only be string or function.`);\n            }\n        } catch (error) {\n            this.stop(\n                new MacroError(\"Trigger\", `ERROR during find trigger:\\n${error.message}`, {\n                    cause: error,\n                })\n            );\n        }\n        return !!this.currentElement;\n    }\n\n    /**\n     * Must not return anything for macro to continue.\n     */\n    async stepAction(element) {\n        const { action } = this.currentStep;\n        if (this.isComplete || !action) {\n            return;\n        }\n        try {\n            return await action(element);\n        } catch (error) {\n            this.stop(\n                new MacroError(\"Action\", `ERROR during perform action:\\n${error.message}`, {\n                    cause: error,\n                })\n            );\n        }\n    }\n\n    get currentStep() {\n        return this.steps[this.currentIndex];\n    }\n\n    get currentElement() {\n        return this.stepElFound[this.currentIndex];\n    }\n\n    set currentElement(value) {\n        this.stepElFound[this.currentIndex] = value;\n    }\n\n    /**\n     * Timer for findTrigger only (not for doing action)\n     */\n    setTimer() {\n        this.clearTimer();\n        const timeout = this.currentStep.timeout || this.timeout;\n        if (timeout > 0) {\n            this.timer = browser.setTimeout(() => {\n                this.stop(\n                    new MacroError(\n                        \"Timeout\",\n                        `TIMEOUT step failed to complete within ${timeout} ms.`\n                    )\n                );\n            }, timeout);\n        }\n    }\n\n    clearTimer() {\n        this.resetDebounce();\n        if (this.timer) {\n            browser.clearTimeout(this.timer);\n        }\n    }\n\n    resetDebounce() {\n        if (this.debouncedAdvance) {\n            browser.clearTimeout(this.debouncedAdvance);\n        }\n    }\n\n    /**\n     * @param {\"next\"|\"mutation\"} from\n     */\n    async debounceAdvance(from) {\n        this.resetDebounce();\n        // Make sure to take the only possible path.\n        // A step always starts with \"next\".\n        // A step can only be continued with \"mutation\".\n        // We abort when the macro is finished or if a mutex occurs afterwards.\n        if (\n            this.isComplete ||\n            (from === \"next\" && this.stepHasStarted[this.currentIndex]) ||\n            (from === \"mutation\" && !this.stepHasStarted[this.currentIndex]) ||\n            (from === \"mutation\" && this.currentElement)\n        ) {\n            return;\n        }\n        // When browser refresh just after the last step.\n        if (!this.currentStep && this.currentIndex === 0) {\n            await delay(300);\n            this.stop();\n        } else if (from === \"next\" && !this.currentStep.trigger) {\n            this.advance();\n        } else {\n            this.debouncedAdvance = browser.setTimeout(\n                () => mutex.exec(() => this.advance()),\n                this.getDebounceDelay()\n            );\n        }\n    }\n\n    stop(error) {\n        this.clearTimer();\n        this.isComplete = true;\n        this.observer.disconnect();\n        if (!this.calledBack) {\n            this.calledBack = true;\n            if (error) {\n                this.onError(error, this.currentStep, this.currentIndex);\n            } else if (this.currentIndex === this.steps.length) {\n                mutex.getUnlockedDef().then(() => {\n                    this.onComplete();\n                });\n            }\n        }\n        return;\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(mutationList);\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: (value) => value.searchable && value.type != \"json\",\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(true);\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            anchor.target = \"_blank\";\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 { markRaw, 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 && markRaw(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, useComponent, 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\nfunction useEarlyExternalListener(target, eventName, handler, eventParams) {\n    const component = useComponent();\n    const boundHandler = handler.bind(component);\n    target.addEventListener(eventName, boundHandler, eventParams);\n    onWillDestroy(() => target.removeEventListener(eventName, boundHandler, eventParams));\n}\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    useEarlyExternalListener(window, \"pointerdown\", pointerDownHandler, { capture: true });\n    useEarlyExternalListener(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        onMounted(() => POPOVERS.set(this.props.target, this.popoverRef.el));\n        onWillDestroy(() => POPOVERS.delete(this.props.target));\n\n        if (!this.props.close) {\n            return;\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    }\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    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 new 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(\n            this.year,\n            this.month - 1,\n            this.day,\n            this.hour,\n            this.minute,\n            this.second\n        );\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 =\n    \"year month day hour minute second microsecond weekday nlyearday yearday\".split(\" \");\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 new 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 new NotSupportedError();\n                }\n            }\n            if (timeDeltaOnRight) {\n                if (left instanceof PyDate || left instanceof PyDateTime) {\n                    return left.add(right);\n                } else {\n                    throw new 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 new 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        onSignatureChange: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        defaultFont: \"\",\n        displaySignatureRatio: 3.0,\n        fontColor: \"DarkBlue\",\n        signatureType: \"signature\",\n        noInputName: false,\n        onSignatureChange: () => {},\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                        this.props.onSignatureChange(this.state.signMode);\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.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    * Loads a signature image from a base64 dataURL and updates the empty state.\n    */\n    async fromDataURL() {\n        await this.signaturePad.fromDataURL(...arguments);\n        this.props.signature.isSignatureEmpty = this.isSignatureEmpty;\n        this.props.onSignatureChange(this.state.signMode);\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        this.props.onSignatureChange(this.state.signMode);\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            this.props.onSignatureChange(this.state.signMode);\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        this.props.onSignatureChange(this.state.signMode);\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\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\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 _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\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\nconst info = Object.create(null);\nconst parsedTemplateExtensions = Object.create(null);\nconst parsedTemplates = Object.create(null);\nconst parser = new DOMParser();\n/** @type {Map<string, Element>} */\nconst processedTemplates = new Map();\nconst registered = new Set();\n/** @type {Record<string, Record<number, ({ templateString: string, url: string })[]>>} */\nconst templateExtensions = Object.create(null);\n/** @type {((document: Document) => void)[]} */\nconst templateProcessors = [];\n/** @type {Record<string, string>} */\nconst templates = Object.create(null);\nlet blockType = null;\nlet blockId = 0;\n/** @type {((url: string) => boolean)[]} */\nlet urlFilters = [];\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\nexport function clearProcessedTemplates() {\n    processedTemplates.clear();\n}\n\n/**\n * @param {string} name\n */\nexport function getTemplate(name) {\n    if (!processedTemplates.has(name)) {\n        processedTemplates.set(name, _getTemplate(name));\n    }\n    return processedTemplates.get(name);\n}\n\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\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", "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\nexport const OPEN_DELAY = 400;\nexport const CLOSE_DELAY = 200;\nexport const SHOW_AFTER_DELAY = 250;\n\nexport const tooltipService = {\n    dependencies: [\"popover\"],\n    start(env, { popover }) {\n        let openTooltipTimeout;\n        let closeTooltip;\n        let showTimer;\n        let target = null;\n        const elementsWithTooltips = new WeakMap();\n\n        /**\n         * Closes the currently opened tooltip if any, or prevent it from opening.\n         */\n        function cleanup() {\n            target = null;\n            browser.clearTimeout(openTooltipTimeout);\n            openTooltipTimeout = null;\n            if (closeTooltip) {\n                closeTooltip();\n                closeTooltip = null;\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            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            cleanup();\n            if (!tooltip && !template) {\n                return;\n            }\n\n            target = el;\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            const element = el.closest(\"[data-tooltip], [data-tooltip-template]\");\n            if (elementsWithTooltips.has(el)) {\n                openTooltip(el, elementsWithTooltips.get(el));\n            } else if (element) {\n                const dataset = element.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(element, 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            openElementsTooltip(ev.target);\n        }\n\n        function cleanupTooltip(ev) {\n            if (target === ev.target.closest(\"[data-tooltip], [data-tooltip-template]\")) {\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            cleanup();\n\n            showTimer = browser.setTimeout(() => {\n                openElementsTooltip(ev.target);\n            }, SHOW_AFTER_DELAY);\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.closest(\"[data-tooltip], [data-tooltip-template]\")) {\n                        if (!ev.target.dataset.tooltipTouchTapToShow) {\n                            browser.clearTimeout(showTimer);\n                            browser.clearTimeout(openTooltipTimeout);\n                        }\n                    }\n                });\n                document.body.addEventListener(\"touchcancel\", (ev) => {\n                    if (ev.target.closest(\"[data-tooltip], [data-tooltip-template]\")) {\n                        if (!ev.target.dataset.tooltipTouchTapToShow) {\n                            browser.clearTimeout(showTimer);\n                            browser.clearTimeout(openTooltipTimeout);\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\", cleanupTooltip, { capture: true });\n            document.body.addEventListener(\"click\", cleanupTooltip, { 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 | Couple) => (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 Couple {\n    constructor(x, y) {\n        this.fst = x;\n        this.snd = y;\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 {Options} [options={}]\n * @param {boolean} [negate=false]\n * @returns {{ tree: Tree, remaimingASTs: AST[] }}\n */\nfunction _construcTree(ASTs, options = {}, negate = false) {\n    const [firstAST, ...tailASTs] = ASTs;\n\n    if (firstAST.type === 1 && firstAST.value === \"!\") {\n        return _construcTree(tailASTs, options, !negate);\n    }\n\n    const tree = { type: firstAST.type === 1 ? \"connector\" : \"condition\" };\n    if (tree.type === \"connector\") {\n        tree.value = firstAST.value;\n        if (options.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                    ...options,\n                    getFieldDef: (p) => options.getFieldDef?.(new Couple(tree.path, p)) || null,\n                });\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                options,\n                options.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 {Options} [options={}]\n * @returns {Tree}\n */\nfunction construcTree(initialASTs, options = {}) {\n    if (!initialASTs.length) {\n        return connector(\"&\");\n    }\n    const { tree } = _construcTree(initialASTs, options);\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\";\nimport { Domain } from \"@web/core/domain\";\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 getDomain(fieldDef) {\n    if (fieldDef.type === \"many2one\") {\n        return [];\n    }\n    try {\n        return new Domain(fieldDef.domain || []).toList();\n    } catch {\n        return [];\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                domain: getDomain(fieldDef),\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 ([\"=\", \"!=\"].includes(operator)) {\n                return {\n                    component: DomainSelectorSingleAutocomplete,\n                    extractProps: ({ value, update }) => ({\n                        resModel: getResModel(fieldDef),\n                        fieldString: fieldDef.string,\n                        update,\n                        resId: value,\n                    }),\n                    isSupported: () => true,\n                    defaultValue: () => false,\n                    shouldResetValue: (value) => value !== false && !isId(value),\n                };\n            } else if ([\"parent_of\", \"child_of\"].includes(operator)) {\n                return makeAutoCompleteEditor(fieldDef);\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    Couple,\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, true);\n        const paths = new Set([...pathsInTree, ...additionalsPath]);\n        const promises = [];\n        const fieldDefs = {};\n        const loadFieldInfoFromMultiplePaths = async (resModel, fieldDefs, path) => {\n            if (typeof path === \"string\" && !(path in fieldDefs)) {\n                const prom = loadFieldInfo(resModel, path).then(({ fieldDef }) => {\n                    fieldDefs[path].fieldDef = fieldDef;\n                    return fieldDef?.relation || null;\n                });\n                fieldDefs[path] = { prom, pathFieldDefs: {}, fieldDef: null };\n                return prom;\n            }\n            if (path instanceof Couple && typeof path.fst === \"string\" && path.fst in fieldDefs) {\n                const resModel = await fieldDefs[path.fst].prom;\n                if (resModel) {\n                    return loadFieldInfoFromMultiplePaths(\n                        resModel,\n                        fieldDefs[path.fst].pathFieldDefs,\n                        path.snd\n                    );\n                }\n            }\n            return null;\n        };\n        for (const path of paths) {\n            promises.push(loadFieldInfoFromMultiplePaths(resModel, fieldDefs, path));\n        }\n        await Promise.all(promises);\n        const _getFieldDef = (path, fieldDefs) => {\n            if (typeof path === \"string\") {\n                return fieldDefs[path].fieldDef;\n            }\n            if (path instanceof Couple && typeof path.fst === \"string\" && path.fst in fieldDefs) {\n                return _getFieldDef(path.snd, fieldDefs[path.fst].pathFieldDefs);\n            }\n            return null;\n        };\n        return (path) => _getFieldDef(path, fieldDefs);\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])\n                  .slice(0, 21)\n                  .map((val, index) =>\n                      index < 20 ? 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, lookInSubTrees = false) {\n    const paths = [];\n    if (tree.type === \"condition\") {\n        paths.push(tree.path);\n        if (lookInSubTrees && isTree(tree.value)) {\n            const subTreePaths = getPathsInTree(tree.value, lookInSubTrees);\n            for (const p of subTreePaths) {\n                paths.push(new Couple(tree.path, p));\n            }\n        }\n    }\n    if (tree.type === \"connector\" && tree.children) {\n        for (const child of tree.children) {\n            paths.push(...getPathsInTree(child, lookInSubTrees));\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 async () => {\n                    // Components are destroyed from top to bottom, meaning that this cleanup is\n                    // called before the ones of children. As a consequence, event handlers added on\n                    // the current active element in children aren't removed yet, and can thus be\n                    // executed if we deactivate that active element right away (e.g. the blur and\n                    // change events could be triggered). For that reason, we wait for a micro-tick.\n                    await Promise.resolve();\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    if (isInternalUser !== undefined) {\n        groupCache.cache[\"base.group_user\"] = Promise.resolve(isInternalUser);\n    }\n    if (isSystem !== undefined) {\n        groupCache.cache[\"base.group_system\"] = Promise.resolve(isSystem);\n    }\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        isInternalUser,\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.isContentEditable\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", "import { markup } from \"@odoo/owl\";\n\nimport { escape } from \"@web/core/utils/strings\";\n\nconst Markup = markup().constructor;\n\n/**\n * Escapes content for HTML. Content is unchanged if it is already a Markup.\n *\n * @param {string|ReturnType<markup>} content\n * @returns {ReturnType<markup>}\n */\nexport function htmlEscape(content) {\n    return content instanceof Markup ? content : markup(escape(content));\n}\n\n/**\n * Checks if a html content is empty. If there are only formatting tags\n * with style attributes or a void content. Famous use case is\n * '<p style=\"...\" class=\"..\"><br></p>' added by some web editor(s).\n * Note that because the use of this method is limited, we ignore the cases\n * like there's one <img> tag in the content. In such case, even if it's the\n * actual content, we consider it empty.\n *\n * @param {string|ReturnType<markup>} content\n * @returns {boolean} true if no content found or if containing only formatting tags\n */\nexport function isHtmlEmpty(content = \"\") {\n    const div = document.createElement(\"div\");\n    setElementContent(div, content);\n    return div.textContent.trim() === \"\";\n}\n\n/**\n * Safely sets content on element. If content was flagged as safe HTML using `markup()` it is set as\n * innerHTML. Otherwise it is set as text.\n *\n * @param {Element} element\n * @param {string|ReturnType<markup>} content\n */\nexport function setElementContent(element, content) {\n    if (content instanceof Markup) {\n        element.innerHTML = content;\n    } else {\n        element.textContent = content;\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", "/**\n * @typedef Position\n * @property {number} x\n * @property {number} y\n */\n\n/**\n * @param {Iterable<HTMLElement>} elements\n * @param {Position} targetPos\n * @returns {HTMLElement | null}\n */\nexport function closest(elements, targetPos) {\n    let closestEl = null;\n    let closestDistance = Infinity;\n    for (const el of elements) {\n        const rect = el.getBoundingClientRect();\n        const distance = getQuadrance(rect, targetPos);\n        if (!closestEl || distance < closestDistance) {\n            closestEl = el;\n            closestDistance = distance;\n        }\n    }\n    return closestEl;\n}\n\n/**\n * rough approximation of a visible element. not perfect (does not take into\n * account opacity = 0 for example), but good enough for our purpose\n *\n * @param {Element} el\n * @returns {boolean}\n */\nexport function isVisible(el) {\n    if (el === document || el === window) {\n        return true;\n    }\n    if (!el) {\n        return false;\n    }\n    let _isVisible = false;\n    if (\"offsetWidth\" in el && \"offsetHeight\" in el) {\n        _isVisible = el.offsetWidth > 0 && el.offsetHeight > 0;\n    } else if (\"getBoundingClientRect\" in el) {\n        // for example, svgelements\n        const rect = el.getBoundingClientRect();\n        _isVisible = rect.width > 0 && rect.height > 0;\n    }\n    if (!_isVisible && getComputedStyle(el).display === \"contents\") {\n        for (const child of el.children) {\n            if (isVisible(child)) {\n                return true;\n            }\n        }\n    }\n    return _isVisible;\n}\n\n/**\n * @param {DOMRect} rect\n * @param {Position} pos\n * @returns {number}\n */\nexport function getQuadrance(rect, pos) {\n    let q = 0;\n    if (pos.x < rect.x) {\n        q += (rect.x - pos.x) ** 2;\n    } else if (rect.x + rect.width < pos.x) {\n        q += (pos.x - (rect.x + rect.width)) ** 2;\n    }\n    if (pos.y < rect.y) {\n        q += (rect.y - pos.y) ** 2;\n    } else if (rect.y + rect.height < pos.y) {\n        q += (pos.y - (rect.y + rect.height)) ** 2;\n    }\n    return q;\n}\n\n/**\n * @param {Element} activeElement\n * @param {String} selector\n * @returns all selected and visible elements present in the activeElement\n */\nexport function getVisibleElements(activeElement, selector) {\n    const visibleElements = [];\n    /** @type {NodeListOf<HTMLElement>} */\n    const elements = activeElement.querySelectorAll(selector);\n    for (const el of elements) {\n        if (isVisible(el)) {\n            visibleElements.push(el);\n        }\n    }\n    return visibleElements;\n}\n\n/**\n * @param {Iterable<HTMLElement>} elements\n * @param {Partial<DOMRect>} targetRect\n * @returns {HTMLElement[]}\n */\nexport function touching(elements, targetRect) {\n    const r1 = { x: 0, y: 0, width: 0, height: 0, ...targetRect };\n    return [...elements].filter((el) => {\n        const r2 = el.getBoundingClientRect();\n        return (\n            r2.x + r2.width >= r1.x &&\n            r2.x <= r1.x + r1.width &&\n            r2.y + r2.height >= r1.y &&\n            r2.y <= r1.y + r1.height\n        );\n    });\n}\n\n// -----------------------------------------------------------------------------\n// Get Tabable Elements\n// -----------------------------------------------------------------------------\n// TODISCUSS:\n//  - leave the following in this file ?\n//  - redefine this selector in tests env with \":not(#qunit *)\" ?\n\n// Following selector is based on this spec: https://html.spec.whatwg.org/multipage/interaction.html#dom-tabindex\nconst TABABLE_SELECTOR = [\n    \"[tabindex]\",\n    \"a\",\n    \"area\",\n    \"button\",\n    \"frame\",\n    \"iframe\",\n    \"input\",\n    \"object\",\n    \"select\",\n    \"textarea\",\n    \"details > summary:nth-child(1)\",\n]\n    .map((sel) => `${sel}:not([tabindex=\"-1\"]):not(:disabled)`)\n    .join(\",\");\n\n/**\n * Returns all focusable elements in the given container.\n *\n * @param {HTMLElement} [container=document.body]\n */\nexport function getTabableElements(container = document.body) {\n    const elements = [...container.querySelectorAll(TABABLE_SELECTOR)].filter(isVisible);\n    /** @type {Record<number, HTMLElement[]>} */\n    const byTabIndex = {};\n    for (const el of [...elements]) {\n        if (!byTabIndex[el.tabIndex]) {\n            byTabIndex[el.tabIndex] = [];\n        }\n        byTabIndex[el.tabIndex].push(el);\n    }\n\n    const withTabIndexZero = byTabIndex[0] || [];\n    delete byTabIndex[0];\n    return [...Object.values(byTabIndex).flat(), ...withTabIndexZero];\n}\n\nexport function getNextTabableElement(container = document.body) {\n    const tabableElements = getTabableElements(container);\n    const index = tabableElements.indexOf(document.activeElement);\n    return index === -1 ? tabableElements[0] : tabableElements[index + 1] || null;\n}\n\nexport function getPreviousTabableElement(container = document.body) {\n    const tabableElements = getTabableElements(container);\n    const index = tabableElements.indexOf(document.activeElement);\n    return index === -1\n        ? tabableElements[tabableElements.length - 1]\n        : tabableElements[index - 1] || null;\n}\n\n/**\n * Gives the button a loading effect by disabling it and adding a `fa` spinner\n * icon. The existing button `fa` icons will be hidden through css.\n *\n * @param {HTMLElement} btnEl - the button to disable/load\n * @return {function} a callback function that will restore the button to its\n *         initial state\n */\nexport function addLoadingEffect(btnEl) {\n    // Note that pe-none is used alongside \"disabled\" so that the behavior is\n    // the same on links not using the \"btn\" class -> pointer-events disabled.\n    btnEl.classList.add(\"o_btn_loading\", \"disabled\", \"pe-none\");\n    btnEl.disabled = true;\n    const loaderEl = document.createElement(\"span\");\n    loaderEl.classList.add(\"fa\", \"fa-refresh\", \"fa-spin\", \"me-2\");\n    btnEl.prepend(loaderEl);\n    return () => {\n        btnEl.classList.remove(\"o_btn_loading\", \"disabled\", \"pe-none\");\n        btnEl.disabled = false;\n        loaderEl.remove();\n    };\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, { access_token, 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 (access_token) {\n        Object.assign(urlParams, { access_token });\n    }\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", "class ClipboardItemImpl {\n    constructor(items, options = {}) {\n        this.items = items;\n        this.options = options;\n    }\n    get presentationStyle() {\n        return this.options.presentationStyle;\n    }\n    get types() {\n        return Object.keys(this.items);\n    }\n    getType(type) {\n        return this.items[type];\n    }\n}\n\nfunction blobToStr(blob) {\n    return new Promise((resolve, reject) => {\n        const reader = new FileReader();\n        reader.addEventListener(\"load\", () => {\n            const { result } = reader;\n            if (typeof result === \"string\") {\n                resolve(result);\n            } else {\n                reject(\"Cannot read Blob as String\");\n            }\n        });\n        reader.addEventListener(\"error\", () => {\n            reject(\"Cannot read Blob\");\n        });\n        reader.readAsText(blob);\n    });\n}\n\nasync function stringify(item) {\n    const strItem = {};\n    for (const type of item.types) {\n        strItem[type] = await blobToStr(item.getType(type));\n    }\n    return strItem;\n}\n\nasync function write(items) {\n    if (!items[0].getType(\"text/plain\")) {\n        throw new Error(\n            `Calling clipboard.write() without a \"text/plain\" type may result in an empty clipboard on some platforms.`\n        );\n    }\n    const strItem = await stringify(items[0]);\n\n    const stubContainer = document.createElement(\"div\");\n    const shadowContainer = stubContainer.attachShadow({ mode: \"open\" });\n    const stub = document.createElement(\"span\");\n    stub.innerText = strItem[\"text/plain\"];\n    shadowContainer.appendChild(stub);\n    document.body.appendChild(stubContainer);\n\n    const selection = document.getSelection();\n    const range = document.createRange();\n    range.selectNodeContents(stub);\n    selection.removeAllRanges();\n    selection.addRange(range);\n\n    const onCopy = (ev) => {\n        for (const type in strItem) {\n            ev.clipboardData.setData(type, strItem[type]);\n        }\n        ev.preventDefault();\n    };\n    document.addEventListener(\"copy\", onCopy);\n    let result;\n    try {\n        result = document.execCommand(\"copy\");\n    } finally {\n        document.removeEventListener(\"copy\", onCopy);\n    }\n\n    selection.removeAllRanges();\n    document.body.removeChild(stubContainer);\n\n    return result;\n}\n\n/**\n * Only attempt to polyfill browsers that partially implement\n * the Clipboard API (aka. Firefox with `clipboard.write()` and\n * `ClipboardItem` behind a feature flag)\n *\n * Spec: https://w3c.github.io/clipboard-apis/\n */\nif (window.navigator.clipboard) {\n    if (!window.navigator.clipboard.write) {\n        window.navigator.clipboard.write = write.bind(window);\n    }\n    if (!window.ClipboardItem) {\n        window.ClipboardItem = ClipboardItemImpl;\n    }\n}\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", "/*! @license DOMPurify 3.1.5 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.1.5/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.DOMPurify = factory());\n})(this, (function () { 'use strict';\n\n  const {\n    entries,\n    setPrototypeOf,\n    isFrozen,\n    getPrototypeOf,\n    getOwnPropertyDescriptor\n  } = Object;\n  let {\n    freeze,\n    seal,\n    create\n  } = Object; // eslint-disable-line import/no-mutable-exports\n  let {\n    apply,\n    construct\n  } = typeof Reflect !== 'undefined' && Reflect;\n  if (!freeze) {\n    freeze = function freeze(x) {\n      return x;\n    };\n  }\n  if (!seal) {\n    seal = function seal(x) {\n      return x;\n    };\n  }\n  if (!apply) {\n    apply = function apply(fun, thisValue, args) {\n      return fun.apply(thisValue, args);\n    };\n  }\n  if (!construct) {\n    construct = function construct(Func, args) {\n      return new Func(...args);\n    };\n  }\n  const arrayForEach = unapply(Array.prototype.forEach);\n  const arrayPop = unapply(Array.prototype.pop);\n  const arrayPush = unapply(Array.prototype.push);\n  const stringToLowerCase = unapply(String.prototype.toLowerCase);\n  const stringToString = unapply(String.prototype.toString);\n  const stringMatch = unapply(String.prototype.match);\n  const stringReplace = unapply(String.prototype.replace);\n  const stringIndexOf = unapply(String.prototype.indexOf);\n  const stringTrim = unapply(String.prototype.trim);\n  const objectHasOwnProperty = unapply(Object.prototype.hasOwnProperty);\n  const regExpTest = unapply(RegExp.prototype.test);\n  const typeErrorCreate = unconstruct(TypeError);\n\n  /**\n   * Creates a new function that calls the given function with a specified thisArg and arguments.\n   *\n   * @param {Function} func - The function to be wrapped and called.\n   * @returns {Function} A new function that calls the given function with a specified thisArg and arguments.\n   */\n  function unapply(func) {\n    return function (thisArg) {\n      for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {\n        args[_key - 1] = arguments[_key];\n      }\n      return apply(func, thisArg, args);\n    };\n  }\n\n  /**\n   * Creates a new function that constructs an instance of the given constructor function with the provided arguments.\n   *\n   * @param {Function} func - The constructor function to be wrapped and called.\n   * @returns {Function} A new function that constructs an instance of the given constructor function with the provided arguments.\n   */\n  function unconstruct(func) {\n    return function () {\n      for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {\n        args[_key2] = arguments[_key2];\n      }\n      return construct(func, args);\n    };\n  }\n\n  /**\n   * Add properties to a lookup table\n   *\n   * @param {Object} set - The set to which elements will be added.\n   * @param {Array} array - The array containing elements to be added to the set.\n   * @param {Function} transformCaseFunc - An optional function to transform the case of each element before adding to the set.\n   * @returns {Object} The modified set with added elements.\n   */\n  function addToSet(set, array) {\n    let transformCaseFunc = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : stringToLowerCase;\n    if (setPrototypeOf) {\n      // Make 'in' and truthy checks like Boolean(set.constructor)\n      // independent of any properties defined on Object.prototype.\n      // Prevent prototype setters from intercepting set as a this value.\n      setPrototypeOf(set, null);\n    }\n    let l = array.length;\n    while (l--) {\n      let element = array[l];\n      if (typeof element === 'string') {\n        const lcElement = transformCaseFunc(element);\n        if (lcElement !== element) {\n          // Config presets (e.g. tags.js, attrs.js) are immutable.\n          if (!isFrozen(array)) {\n            array[l] = lcElement;\n          }\n          element = lcElement;\n        }\n      }\n      set[element] = true;\n    }\n    return set;\n  }\n\n  /**\n   * Clean up an array to harden against CSPP\n   *\n   * @param {Array} array - The array to be cleaned.\n   * @returns {Array} The cleaned version of the array\n   */\n  function cleanArray(array) {\n    for (let index = 0; index < array.length; index++) {\n      const isPropertyExist = objectHasOwnProperty(array, index);\n      if (!isPropertyExist) {\n        array[index] = null;\n      }\n    }\n    return array;\n  }\n\n  /**\n   * Shallow clone an object\n   *\n   * @param {Object} object - The object to be cloned.\n   * @returns {Object} A new object that copies the original.\n   */\n  function clone(object) {\n    const newObject = create(null);\n    for (const [property, value] of entries(object)) {\n      const isPropertyExist = objectHasOwnProperty(object, property);\n      if (isPropertyExist) {\n        if (Array.isArray(value)) {\n          newObject[property] = cleanArray(value);\n        } else if (value && typeof value === 'object' && value.constructor === Object) {\n          newObject[property] = clone(value);\n        } else {\n          newObject[property] = value;\n        }\n      }\n    }\n    return newObject;\n  }\n\n  /**\n   * This method automatically checks if the prop is function or getter and behaves accordingly.\n   *\n   * @param {Object} object - The object to look up the getter function in its prototype chain.\n   * @param {String} prop - The property name for which to find the getter function.\n   * @returns {Function} The getter function found in the prototype chain or a fallback function.\n   */\n  function lookupGetter(object, prop) {\n    while (object !== null) {\n      const desc = getOwnPropertyDescriptor(object, prop);\n      if (desc) {\n        if (desc.get) {\n          return unapply(desc.get);\n        }\n        if (typeof desc.value === 'function') {\n          return unapply(desc.value);\n        }\n      }\n      object = getPrototypeOf(object);\n    }\n    function fallbackValue() {\n      return null;\n    }\n    return fallbackValue;\n  }\n\n  const html$1 = freeze(['a', 'abbr', 'acronym', 'address', 'area', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'decorator', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meter', 'nav', 'nobr', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'section', 'select', 'shadow', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr']);\n\n  // SVG\n  const svg$1 = freeze(['svg', 'a', 'altglyph', 'altglyphdef', 'altglyphitem', 'animatecolor', 'animatemotion', 'animatetransform', 'circle', 'clippath', 'defs', 'desc', 'ellipse', 'filter', 'font', 'g', 'glyph', 'glyphref', 'hkern', 'image', 'line', 'lineargradient', 'marker', 'mask', 'metadata', 'mpath', 'path', 'pattern', 'polygon', 'polyline', 'radialgradient', 'rect', 'stop', 'style', 'switch', 'symbol', 'text', 'textpath', 'title', 'tref', 'tspan', 'view', 'vkern']);\n  const svgFilters = freeze(['feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight', 'feDropShadow', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence']);\n\n  // List of SVG elements that are disallowed by default.\n  // We still need to know them so that we can do namespace\n  // checks properly in case one wants to add them to\n  // allow-list.\n  const svgDisallowed = freeze(['animate', 'color-profile', 'cursor', 'discard', 'font-face', 'font-face-format', 'font-face-name', 'font-face-src', 'font-face-uri', 'foreignobject', 'hatch', 'hatchpath', 'mesh', 'meshgradient', 'meshpatch', 'meshrow', 'missing-glyph', 'script', 'set', 'solidcolor', 'unknown', 'use']);\n  const mathMl$1 = freeze(['math', 'menclose', 'merror', 'mfenced', 'mfrac', 'mglyph', 'mi', 'mlabeledtr', 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', 'mroot', 'mrow', 'ms', 'mspace', 'msqrt', 'mstyle', 'msub', 'msup', 'msubsup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', 'munderover', 'mprescripts']);\n\n  // Similarly to SVG, we want to know all MathML elements,\n  // even those that we disallow by default.\n  const mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']);\n  const text = freeze(['#text']);\n\n  const html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'pattern', 'placeholder', 'playsinline', 'popover', 'popovertarget', 'popovertargetaction', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wrap', 'xmlns', 'slot']);\n  const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dur', 'edgemode', 'elevation', 'end', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']);\n  const mathMl = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnsalign', 'columnlines', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lspace', 'lquote', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']);\n  const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);\n\n  // eslint-disable-next-line unicorn/better-regex\n  const MUSTACHE_EXPR = seal(/\\{\\{[\\w\\W]*|[\\w\\W]*\\}\\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode\n  const ERB_EXPR = seal(/<%[\\w\\W]*|[\\w\\W]*%>/gm);\n  const TMPLIT_EXPR = seal(/\\${[\\w\\W]*}/gm);\n  const DATA_ATTR = seal(/^data-[\\-\\w.\\u00B7-\\uFFFF]/); // eslint-disable-line no-useless-escape\n  const ARIA_ATTR = seal(/^aria-[\\-\\w]+$/); // eslint-disable-line no-useless-escape\n  const IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\\-]+(?:[^a-z+.\\-:]|$))/i // eslint-disable-line no-useless-escape\n  );\n\n  const IS_SCRIPT_OR_DATA = seal(/^(?:\\w+script|data):/i);\n  const ATTR_WHITESPACE = seal(/[\\u0000-\\u0020\\u00A0\\u1680\\u180E\\u2000-\\u2029\\u205F\\u3000]/g // eslint-disable-line no-control-regex\n  );\n\n  const DOCTYPE_NAME = seal(/^html$/i);\n  const CUSTOM_ELEMENT = seal(/^[a-z][.\\w]*(-[.\\w]+)+$/i);\n\n  var EXPRESSIONS = /*#__PURE__*/Object.freeze({\n    __proto__: null,\n    MUSTACHE_EXPR: MUSTACHE_EXPR,\n    ERB_EXPR: ERB_EXPR,\n    TMPLIT_EXPR: TMPLIT_EXPR,\n    DATA_ATTR: DATA_ATTR,\n    ARIA_ATTR: ARIA_ATTR,\n    IS_ALLOWED_URI: IS_ALLOWED_URI,\n    IS_SCRIPT_OR_DATA: IS_SCRIPT_OR_DATA,\n    ATTR_WHITESPACE: ATTR_WHITESPACE,\n    DOCTYPE_NAME: DOCTYPE_NAME,\n    CUSTOM_ELEMENT: CUSTOM_ELEMENT\n  });\n\n  // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType\n  const NODE_TYPE = {\n    element: 1,\n    attribute: 2,\n    text: 3,\n    cdataSection: 4,\n    entityReference: 5,\n    // Deprecated\n    entityNode: 6,\n    // Deprecated\n    progressingInstruction: 7,\n    comment: 8,\n    document: 9,\n    documentType: 10,\n    documentFragment: 11,\n    notation: 12 // Deprecated\n  };\n\n  const getGlobal = function getGlobal() {\n    return typeof window === 'undefined' ? null : window;\n  };\n\n  /**\n   * Creates a no-op policy for internal use only.\n   * Don't export this function outside this module!\n   * @param {TrustedTypePolicyFactory} trustedTypes The policy factory.\n   * @param {HTMLScriptElement} purifyHostElement The Script element used to load DOMPurify (to determine policy name suffix).\n   * @return {TrustedTypePolicy} The policy created (or null, if Trusted Types\n   * are not supported or creating the policy failed).\n   */\n  const _createTrustedTypesPolicy = function _createTrustedTypesPolicy(trustedTypes, purifyHostElement) {\n    if (typeof trustedTypes !== 'object' || typeof trustedTypes.createPolicy !== 'function') {\n      return null;\n    }\n\n    // Allow the callers to control the unique policy name\n    // by adding a data-tt-policy-suffix to the script element with the DOMPurify.\n    // Policy creation with duplicate names throws in Trusted Types.\n    let suffix = null;\n    const ATTR_NAME = 'data-tt-policy-suffix';\n    if (purifyHostElement && purifyHostElement.hasAttribute(ATTR_NAME)) {\n      suffix = purifyHostElement.getAttribute(ATTR_NAME);\n    }\n    const policyName = 'dompurify' + (suffix ? '#' + suffix : '');\n    try {\n      return trustedTypes.createPolicy(policyName, {\n        createHTML(html) {\n          return html;\n        },\n        createScriptURL(scriptUrl) {\n          return scriptUrl;\n        }\n      });\n    } catch (_) {\n      // Policy creation failed (most likely another DOMPurify script has\n      // already run). Skip creating the policy, as this will only cause errors\n      // if TT are enforced.\n      console.warn('TrustedTypes policy ' + policyName + ' could not be created.');\n      return null;\n    }\n  };\n  function createDOMPurify() {\n    let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();\n    const DOMPurify = root => createDOMPurify(root);\n\n    /**\n     * Version label, exposed for easier checks\n     * if DOMPurify is up to date or not\n     */\n    DOMPurify.version = '3.1.5';\n\n    /**\n     * Array of elements that DOMPurify removed during sanitation.\n     * Empty if nothing was removed.\n     */\n    DOMPurify.removed = [];\n    if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document) {\n      // Not running in a browser, provide a factory function\n      // so that you can pass your own Window\n      DOMPurify.isSupported = false;\n      return DOMPurify;\n    }\n    let {\n      document\n    } = window;\n    const originalDocument = document;\n    const currentScript = originalDocument.currentScript;\n    const {\n      DocumentFragment,\n      HTMLTemplateElement,\n      Node,\n      Element,\n      NodeFilter,\n      NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap,\n      HTMLFormElement,\n      DOMParser,\n      trustedTypes\n    } = window;\n    const ElementPrototype = Element.prototype;\n    const cloneNode = lookupGetter(ElementPrototype, 'cloneNode');\n    const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');\n    const getChildNodes = lookupGetter(ElementPrototype, 'childNodes');\n    const getParentNode = lookupGetter(ElementPrototype, 'parentNode');\n\n    // As per issue #47, the web-components registry is inherited by a\n    // new document created via createHTMLDocument. As per the spec\n    // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)\n    // a new empty registry is used when creating a template contents owner\n    // document, so we use that as our parent document to ensure nothing\n    // is inherited.\n    if (typeof HTMLTemplateElement === 'function') {\n      const template = document.createElement('template');\n      if (template.content && template.content.ownerDocument) {\n        document = template.content.ownerDocument;\n      }\n    }\n    let trustedTypesPolicy;\n    let emptyHTML = '';\n    const {\n      implementation,\n      createNodeIterator,\n      createDocumentFragment,\n      getElementsByTagName\n    } = document;\n    const {\n      importNode\n    } = originalDocument;\n    let hooks = {};\n\n    /**\n     * Expose whether this browser supports running the full DOMPurify.\n     */\n    DOMPurify.isSupported = typeof entries === 'function' && typeof getParentNode === 'function' && implementation && implementation.createHTMLDocument !== undefined;\n    const {\n      MUSTACHE_EXPR,\n      ERB_EXPR,\n      TMPLIT_EXPR,\n      DATA_ATTR,\n      ARIA_ATTR,\n      IS_SCRIPT_OR_DATA,\n      ATTR_WHITESPACE,\n      CUSTOM_ELEMENT\n    } = EXPRESSIONS;\n    let {\n      IS_ALLOWED_URI: IS_ALLOWED_URI$1\n    } = EXPRESSIONS;\n\n    /**\n     * We consider the elements and attributes below to be safe. Ideally\n     * don't add any new ones but feel free to remove unwanted ones.\n     */\n\n    /* allowed element names */\n    let ALLOWED_TAGS = null;\n    const DEFAULT_ALLOWED_TAGS = addToSet({}, [...html$1, ...svg$1, ...svgFilters, ...mathMl$1, ...text]);\n\n    /* Allowed attribute names */\n    let ALLOWED_ATTR = null;\n    const DEFAULT_ALLOWED_ATTR = addToSet({}, [...html, ...svg, ...mathMl, ...xml]);\n\n    /*\n     * Configure how DOMPUrify should handle custom elements and their attributes as well as customized built-in elements.\n     * @property {RegExp|Function|null} tagNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any custom elements)\n     * @property {RegExp|Function|null} attributeNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any attributes not on the allow list)\n     * @property {boolean} allowCustomizedBuiltInElements allow custom elements derived from built-ins if they pass CUSTOM_ELEMENT_HANDLING.tagNameCheck. Default: `false`.\n     */\n    let CUSTOM_ELEMENT_HANDLING = Object.seal(create(null, {\n      tagNameCheck: {\n        writable: true,\n        configurable: false,\n        enumerable: true,\n        value: null\n      },\n      attributeNameCheck: {\n        writable: true,\n        configurable: false,\n        enumerable: true,\n        value: null\n      },\n      allowCustomizedBuiltInElements: {\n        writable: true,\n        configurable: false,\n        enumerable: true,\n        value: false\n      }\n    }));\n\n    /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */\n    let FORBID_TAGS = null;\n\n    /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */\n    let FORBID_ATTR = null;\n\n    /* Decide if ARIA attributes are okay */\n    let ALLOW_ARIA_ATTR = true;\n\n    /* Decide if custom data attributes are okay */\n    let ALLOW_DATA_ATTR = true;\n\n    /* Decide if unknown protocols are okay */\n    let ALLOW_UNKNOWN_PROTOCOLS = false;\n\n    /* Decide if self-closing tags in attributes are allowed.\n     * Usually removed due to a mXSS issue in jQuery 3.0 */\n    let ALLOW_SELF_CLOSE_IN_ATTR = true;\n\n    /* Output should be safe for common template engines.\n     * This means, DOMPurify removes data attributes, mustaches and ERB\n     */\n    let SAFE_FOR_TEMPLATES = false;\n\n    /* Output should be safe even for XML used within HTML and alike.\n     * This means, DOMPurify removes comments when containing risky content.\n     */\n    let SAFE_FOR_XML = true;\n\n    /* Decide if document with <html>... should be returned */\n    let WHOLE_DOCUMENT = false;\n\n    /* Track whether config is already set on this instance of DOMPurify. */\n    let SET_CONFIG = false;\n\n    /* Decide if all elements (e.g. style, script) must be children of\n     * document.body. By default, browsers might move them to document.head */\n    let FORCE_BODY = false;\n\n    /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html\n     * string (or a TrustedHTML object if Trusted Types are supported).\n     * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead\n     */\n    let RETURN_DOM = false;\n\n    /* Decide if a DOM `DocumentFragment` should be returned, instead of a html\n     * string  (or a TrustedHTML object if Trusted Types are supported) */\n    let RETURN_DOM_FRAGMENT = false;\n\n    /* Try to return a Trusted Type object instead of a string, return a string in\n     * case Trusted Types are not supported  */\n    let RETURN_TRUSTED_TYPE = false;\n\n    /* Output should be free from DOM clobbering attacks?\n     * This sanitizes markups named with colliding, clobberable built-in DOM APIs.\n     */\n    let SANITIZE_DOM = true;\n\n    /* Achieve full DOM Clobbering protection by isolating the namespace of named\n     * properties and JS variables, mitigating attacks that abuse the HTML/DOM spec rules.\n     *\n     * HTML/DOM spec rules that enable DOM Clobbering:\n     *   - Named Access on Window (\u00a77.3.3)\n     *   - DOM Tree Accessors (\u00a73.1.5)\n     *   - Form Element Parent-Child Relations (\u00a74.10.3)\n     *   - Iframe srcdoc / Nested WindowProxies (\u00a74.8.5)\n     *   - HTMLCollection (\u00a74.2.10.2)\n     *\n     * Namespace isolation is implemented by prefixing `id` and `name` attributes\n     * with a constant string, i.e., `user-content-`\n     */\n    let SANITIZE_NAMED_PROPS = false;\n    const SANITIZE_NAMED_PROPS_PREFIX = 'user-content-';\n\n    /* Keep element content when removing element? */\n    let KEEP_CONTENT = true;\n\n    /* If a `Node` is passed to sanitize(), then performs sanitization in-place instead\n     * of importing it into a new Document and returning a sanitized copy */\n    let IN_PLACE = false;\n\n    /* Allow usage of profiles like html, svg and mathMl */\n    let USE_PROFILES = {};\n\n    /* Tags to ignore content of when KEEP_CONTENT is true */\n    let FORBID_CONTENTS = null;\n    const DEFAULT_FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'noscript', 'plaintext', 'script', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']);\n\n    /* Tags that are safe for data: URIs */\n    let DATA_URI_TAGS = null;\n    const DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']);\n\n    /* Attributes safe for values like \"javascript:\" */\n    let URI_SAFE_ATTRIBUTES = null;\n    const DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ['alt', 'class', 'for', 'id', 'label', 'name', 'pattern', 'placeholder', 'role', 'summary', 'title', 'value', 'style', 'xmlns']);\n    const MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';\n    const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';\n    const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';\n    /* Document namespace */\n    let NAMESPACE = HTML_NAMESPACE;\n    let IS_EMPTY_INPUT = false;\n\n    /* Allowed XHTML+XML namespaces */\n    let ALLOWED_NAMESPACES = null;\n    const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString);\n\n    /* Parsing of strict XHTML documents */\n    let PARSER_MEDIA_TYPE = null;\n    const SUPPORTED_PARSER_MEDIA_TYPES = ['application/xhtml+xml', 'text/html'];\n    const DEFAULT_PARSER_MEDIA_TYPE = 'text/html';\n    let transformCaseFunc = null;\n\n    /* Keep a reference to config to pass to hooks */\n    let CONFIG = null;\n\n    /* Ideally, do not touch anything below this line */\n    /* ______________________________________________ */\n\n    const formElement = document.createElement('form');\n    const isRegexOrFunction = function isRegexOrFunction(testValue) {\n      return testValue instanceof RegExp || testValue instanceof Function;\n    };\n\n    /**\n     * _parseConfig\n     *\n     * @param  {Object} cfg optional config literal\n     */\n    // eslint-disable-next-line complexity\n    const _parseConfig = function _parseConfig() {\n      let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n      if (CONFIG && CONFIG === cfg) {\n        return;\n      }\n\n      /* Shield configuration object from tampering */\n      if (!cfg || typeof cfg !== 'object') {\n        cfg = {};\n      }\n\n      /* Shield configuration object from prototype pollution */\n      cfg = clone(cfg);\n      PARSER_MEDIA_TYPE =\n      // eslint-disable-next-line unicorn/prefer-includes\n      SUPPORTED_PARSER_MEDIA_TYPES.indexOf(cfg.PARSER_MEDIA_TYPE) === -1 ? DEFAULT_PARSER_MEDIA_TYPE : cfg.PARSER_MEDIA_TYPE;\n\n      // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is.\n      transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase;\n\n      /* Set configuration parameters */\n      ALLOWED_TAGS = objectHasOwnProperty(cfg, 'ALLOWED_TAGS') ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS;\n      ALLOWED_ATTR = objectHasOwnProperty(cfg, 'ALLOWED_ATTR') ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR;\n      ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, 'ALLOWED_NAMESPACES') ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES;\n      URI_SAFE_ATTRIBUTES = objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES),\n      // eslint-disable-line indent\n      cfg.ADD_URI_SAFE_ATTR,\n      // eslint-disable-line indent\n      transformCaseFunc // eslint-disable-line indent\n      ) // eslint-disable-line indent\n      : DEFAULT_URI_SAFE_ATTRIBUTES;\n      DATA_URI_TAGS = objectHasOwnProperty(cfg, 'ADD_DATA_URI_TAGS') ? addToSet(clone(DEFAULT_DATA_URI_TAGS),\n      // eslint-disable-line indent\n      cfg.ADD_DATA_URI_TAGS,\n      // eslint-disable-line indent\n      transformCaseFunc // eslint-disable-line indent\n      ) // eslint-disable-line indent\n      : DEFAULT_DATA_URI_TAGS;\n      FORBID_CONTENTS = objectHasOwnProperty(cfg, 'FORBID_CONTENTS') ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS;\n      FORBID_TAGS = objectHasOwnProperty(cfg, 'FORBID_TAGS') ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : {};\n      FORBID_ATTR = objectHasOwnProperty(cfg, 'FORBID_ATTR') ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : {};\n      USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES : false;\n      ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true\n      ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true\n      ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false\n      ALLOW_SELF_CLOSE_IN_ATTR = cfg.ALLOW_SELF_CLOSE_IN_ATTR !== false; // Default true\n      SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false\n      SAFE_FOR_XML = cfg.SAFE_FOR_XML !== false; // Default true\n      WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false\n      RETURN_DOM = cfg.RETURN_DOM || false; // Default false\n      RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false\n      RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; // Default false\n      FORCE_BODY = cfg.FORCE_BODY || false; // Default false\n      SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true\n      SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false\n      KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true\n      IN_PLACE = cfg.IN_PLACE || false; // Default false\n      IS_ALLOWED_URI$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI;\n      NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE;\n      CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || {};\n      if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)) {\n        CUSTOM_ELEMENT_HANDLING.tagNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck;\n      }\n      if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)) {\n        CUSTOM_ELEMENT_HANDLING.attributeNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck;\n      }\n      if (cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements === 'boolean') {\n        CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements;\n      }\n      if (SAFE_FOR_TEMPLATES) {\n        ALLOW_DATA_ATTR = false;\n      }\n      if (RETURN_DOM_FRAGMENT) {\n        RETURN_DOM = true;\n      }\n\n      /* Parse profile info */\n      if (USE_PROFILES) {\n        ALLOWED_TAGS = addToSet({}, text);\n        ALLOWED_ATTR = [];\n        if (USE_PROFILES.html === true) {\n          addToSet(ALLOWED_TAGS, html$1);\n          addToSet(ALLOWED_ATTR, html);\n        }\n        if (USE_PROFILES.svg === true) {\n          addToSet(ALLOWED_TAGS, svg$1);\n          addToSet(ALLOWED_ATTR, svg);\n          addToSet(ALLOWED_ATTR, xml);\n        }\n        if (USE_PROFILES.svgFilters === true) {\n          addToSet(ALLOWED_TAGS, svgFilters);\n          addToSet(ALLOWED_ATTR, svg);\n          addToSet(ALLOWED_ATTR, xml);\n        }\n        if (USE_PROFILES.mathMl === true) {\n          addToSet(ALLOWED_TAGS, mathMl$1);\n          addToSet(ALLOWED_ATTR, mathMl);\n          addToSet(ALLOWED_ATTR, xml);\n        }\n      }\n\n      /* Merge configuration parameters */\n      if (cfg.ADD_TAGS) {\n        if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {\n          ALLOWED_TAGS = clone(ALLOWED_TAGS);\n        }\n        addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc);\n      }\n      if (cfg.ADD_ATTR) {\n        if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {\n          ALLOWED_ATTR = clone(ALLOWED_ATTR);\n        }\n        addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);\n      }\n      if (cfg.ADD_URI_SAFE_ATTR) {\n        addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);\n      }\n      if (cfg.FORBID_CONTENTS) {\n        if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {\n          FORBID_CONTENTS = clone(FORBID_CONTENTS);\n        }\n        addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);\n      }\n\n      /* Add #text in case KEEP_CONTENT is set to true */\n      if (KEEP_CONTENT) {\n        ALLOWED_TAGS['#text'] = true;\n      }\n\n      /* Add html, head and body to ALLOWED_TAGS in case WHOLE_DOCUMENT is true */\n      if (WHOLE_DOCUMENT) {\n        addToSet(ALLOWED_TAGS, ['html', 'head', 'body']);\n      }\n\n      /* Add tbody to ALLOWED_TAGS in case tables are permitted, see #286, #365 */\n      if (ALLOWED_TAGS.table) {\n        addToSet(ALLOWED_TAGS, ['tbody']);\n        delete FORBID_TAGS.tbody;\n      }\n      if (cfg.TRUSTED_TYPES_POLICY) {\n        if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') {\n          throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a \"createHTML\" hook.');\n        }\n        if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== 'function') {\n          throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a \"createScriptURL\" hook.');\n        }\n\n        // Overwrite existing TrustedTypes policy.\n        trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY;\n\n        // Sign local variables required by `sanitize`.\n        emptyHTML = trustedTypesPolicy.createHTML('');\n      } else {\n        // Uninitialized policy, attempt to initialize the internal dompurify policy.\n        if (trustedTypesPolicy === undefined) {\n          trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);\n        }\n\n        // If creating the internal policy succeeded sign internal variables.\n        if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') {\n          emptyHTML = trustedTypesPolicy.createHTML('');\n        }\n      }\n\n      // Prevent further manipulation of configuration.\n      // Not available in IE8, Safari 5, etc.\n      if (freeze) {\n        freeze(cfg);\n      }\n      CONFIG = cfg;\n    };\n    const MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']);\n    const HTML_INTEGRATION_POINTS = addToSet({}, ['foreignobject', 'annotation-xml']);\n\n    // Certain elements are allowed in both SVG and HTML\n    // namespace. We need to specify them explicitly\n    // so that they don't get erroneously deleted from\n    // HTML namespace.\n    const COMMON_SVG_AND_HTML_ELEMENTS = addToSet({}, ['title', 'style', 'font', 'a', 'script']);\n\n    /* Keep track of all possible SVG and MathML tags\n     * so that we can perform the namespace checks\n     * correctly. */\n    const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]);\n    const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]);\n\n    /**\n     * @param  {Element} element a DOM element whose namespace is being checked\n     * @returns {boolean} Return false if the element has a\n     *  namespace that a spec-compliant parser would never\n     *  return. Return true otherwise.\n     */\n    const _checkValidNamespace = function _checkValidNamespace(element) {\n      let parent = getParentNode(element);\n\n      // In JSDOM, if we're inside shadow DOM, then parentNode\n      // can be null. We just simulate parent in this case.\n      if (!parent || !parent.tagName) {\n        parent = {\n          namespaceURI: NAMESPACE,\n          tagName: 'template'\n        };\n      }\n      const tagName = stringToLowerCase(element.tagName);\n      const parentTagName = stringToLowerCase(parent.tagName);\n      if (!ALLOWED_NAMESPACES[element.namespaceURI]) {\n        return false;\n      }\n      if (element.namespaceURI === SVG_NAMESPACE) {\n        // The only way to switch from HTML namespace to SVG\n        // is via <svg>. If it happens via any other tag, then\n        // it should be killed.\n        if (parent.namespaceURI === HTML_NAMESPACE) {\n          return tagName === 'svg';\n        }\n\n        // The only way to switch from MathML to SVG is via`\n        // svg if parent is either <annotation-xml> or MathML\n        // text integration points.\n        if (parent.namespaceURI === MATHML_NAMESPACE) {\n          return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);\n        }\n\n        // We only allow elements that are defined in SVG\n        // spec. All others are disallowed in SVG namespace.\n        return Boolean(ALL_SVG_TAGS[tagName]);\n      }\n      if (element.namespaceURI === MATHML_NAMESPACE) {\n        // The only way to switch from HTML namespace to MathML\n        // is via <math>. If it happens via any other tag, then\n        // it should be killed.\n        if (parent.namespaceURI === HTML_NAMESPACE) {\n          return tagName === 'math';\n        }\n\n        // The only way to switch from SVG to MathML is via\n        // <math> and HTML integration points\n        if (parent.namespaceURI === SVG_NAMESPACE) {\n          return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];\n        }\n\n        // We only allow elements that are defined in MathML\n        // spec. All others are disallowed in MathML namespace.\n        return Boolean(ALL_MATHML_TAGS[tagName]);\n      }\n      if (element.namespaceURI === HTML_NAMESPACE) {\n        // The only way to switch from SVG to HTML is via\n        // HTML integration points, and from MathML to HTML\n        // is via MathML text integration points\n        if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {\n          return false;\n        }\n        if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {\n          return false;\n        }\n\n        // We disallow tags that are specific for MathML\n        // or SVG and should never appear in HTML namespace\n        return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);\n      }\n\n      // For XHTML and XML documents that support custom namespaces\n      if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && ALLOWED_NAMESPACES[element.namespaceURI]) {\n        return true;\n      }\n\n      // The code should never reach this place (this means\n      // that the element somehow got namespace that is not\n      // HTML, SVG, MathML or allowed via ALLOWED_NAMESPACES).\n      // Return false just in case.\n      return false;\n    };\n\n    /**\n     * _forceRemove\n     *\n     * @param  {Node} node a DOM node\n     */\n    const _forceRemove = function _forceRemove(node) {\n      arrayPush(DOMPurify.removed, {\n        element: node\n      });\n      try {\n        // eslint-disable-next-line unicorn/prefer-dom-node-remove\n        node.parentNode.removeChild(node);\n      } catch (_) {\n        node.remove();\n      }\n    };\n\n    /**\n     * _removeAttribute\n     *\n     * @param  {String} name an Attribute name\n     * @param  {Node} node a DOM node\n     */\n    const _removeAttribute = function _removeAttribute(name, node) {\n      try {\n        arrayPush(DOMPurify.removed, {\n          attribute: node.getAttributeNode(name),\n          from: node\n        });\n      } catch (_) {\n        arrayPush(DOMPurify.removed, {\n          attribute: null,\n          from: node\n        });\n      }\n      node.removeAttribute(name);\n\n      // We void attribute values for unremovable \"is\"\" attributes\n      if (name === 'is' && !ALLOWED_ATTR[name]) {\n        if (RETURN_DOM || RETURN_DOM_FRAGMENT) {\n          try {\n            _forceRemove(node);\n          } catch (_) {}\n        } else {\n          try {\n            node.setAttribute(name, '');\n          } catch (_) {}\n        }\n      }\n    };\n\n    /**\n     * _initDocument\n     *\n     * @param  {String} dirty a string of dirty markup\n     * @return {Document} a DOM, filled with the dirty markup\n     */\n    const _initDocument = function _initDocument(dirty) {\n      /* Create a HTML document */\n      let doc = null;\n      let leadingWhitespace = null;\n      if (FORCE_BODY) {\n        dirty = '<remove></remove>' + dirty;\n      } else {\n        /* If FORCE_BODY isn't used, leading whitespace needs to be preserved manually */\n        const matches = stringMatch(dirty, /^[\\r\\n\\t ]+/);\n        leadingWhitespace = matches && matches[0];\n      }\n      if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && NAMESPACE === HTML_NAMESPACE) {\n        // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict)\n        dirty = '<html xmlns=\"http://www.w3.org/1999/xhtml\"><head></head><body>' + dirty + '</body></html>';\n      }\n      const dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty;\n      /*\n       * Use the DOMParser API by default, fallback later if needs be\n       * DOMParser not work for svg when has multiple root element.\n       */\n      if (NAMESPACE === HTML_NAMESPACE) {\n        try {\n          doc = new DOMParser().parseFromString(dirtyPayload, PARSER_MEDIA_TYPE);\n        } catch (_) {}\n      }\n\n      /* Use createHTMLDocument in case DOMParser is not available */\n      if (!doc || !doc.documentElement) {\n        doc = implementation.createDocument(NAMESPACE, 'template', null);\n        try {\n          doc.documentElement.innerHTML = IS_EMPTY_INPUT ? emptyHTML : dirtyPayload;\n        } catch (_) {\n          // Syntax error if dirtyPayload is invalid xml\n        }\n      }\n      const body = doc.body || doc.documentElement;\n      if (dirty && leadingWhitespace) {\n        body.insertBefore(document.createTextNode(leadingWhitespace), body.childNodes[0] || null);\n      }\n\n      /* Work on whole document or just its body */\n      if (NAMESPACE === HTML_NAMESPACE) {\n        return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? 'html' : 'body')[0];\n      }\n      return WHOLE_DOCUMENT ? doc.documentElement : body;\n    };\n\n    /**\n     * Creates a NodeIterator object that you can use to traverse filtered lists of nodes or elements in a document.\n     *\n     * @param  {Node} root The root element or node to start traversing on.\n     * @return {NodeIterator} The created NodeIterator\n     */\n    const _createNodeIterator = function _createNodeIterator(root) {\n      return createNodeIterator.call(root.ownerDocument || root, root,\n      // eslint-disable-next-line no-bitwise\n      NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);\n    };\n\n    /**\n     * _isClobbered\n     *\n     * @param  {Node} elm element to check for clobbering attacks\n     * @return {Boolean} true if clobbered, false if safe\n     */\n    const _isClobbered = function _isClobbered(elm) {\n      return elm instanceof HTMLFormElement && (typeof elm.nodeName !== 'string' || typeof elm.textContent !== 'string' || typeof elm.removeChild !== 'function' || !(elm.attributes instanceof NamedNodeMap) || typeof elm.removeAttribute !== 'function' || typeof elm.setAttribute !== 'function' || typeof elm.namespaceURI !== 'string' || typeof elm.insertBefore !== 'function' || typeof elm.hasChildNodes !== 'function');\n    };\n\n    /**\n     * Checks whether the given object is a DOM node.\n     *\n     * @param  {Node} object object to check whether it's a DOM node\n     * @return {Boolean} true is object is a DOM node\n     */\n    const _isNode = function _isNode(object) {\n      return typeof Node === 'function' && object instanceof Node;\n    };\n\n    /**\n     * _executeHook\n     * Execute user configurable hooks\n     *\n     * @param  {String} entryPoint  Name of the hook's entry point\n     * @param  {Node} currentNode node to work on with the hook\n     * @param  {Object} data additional hook parameters\n     */\n    const _executeHook = function _executeHook(entryPoint, currentNode, data) {\n      if (!hooks[entryPoint]) {\n        return;\n      }\n      arrayForEach(hooks[entryPoint], hook => {\n        hook.call(DOMPurify, currentNode, data, CONFIG);\n      });\n    };\n\n    /**\n     * _sanitizeElements\n     *\n     * @protect nodeName\n     * @protect textContent\n     * @protect removeChild\n     *\n     * @param   {Node} currentNode to check for permission to exist\n     * @return  {Boolean} true if node was killed, false if left alive\n     */\n    const _sanitizeElements = function _sanitizeElements(currentNode) {\n      let content = null;\n\n      /* Execute a hook if present */\n      _executeHook('beforeSanitizeElements', currentNode, null);\n\n      /* Check if element is clobbered or can clobber */\n      if (_isClobbered(currentNode)) {\n        _forceRemove(currentNode);\n        return true;\n      }\n\n      /* Now let's check the element's type and name */\n      const tagName = transformCaseFunc(currentNode.nodeName);\n\n      /* Execute a hook if present */\n      _executeHook('uponSanitizeElement', currentNode, {\n        tagName,\n        allowedTags: ALLOWED_TAGS\n      });\n\n      /* Detect mXSS attempts abusing namespace confusion */\n      if (currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\\w]/g, currentNode.innerHTML) && regExpTest(/<[/\\w]/g, currentNode.textContent)) {\n        _forceRemove(currentNode);\n        return true;\n      }\n\n      /* Remove any ocurrence of processing instructions */\n      if (currentNode.nodeType === NODE_TYPE.progressingInstruction) {\n        _forceRemove(currentNode);\n        return true;\n      }\n\n      /* Remove any kind of possibly harmful comments */\n      if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(/<[/\\w]/g, currentNode.data)) {\n        _forceRemove(currentNode);\n        return true;\n      }\n\n      /* Remove element if anything forbids its presence */\n      if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {\n        /* Check if we have a custom element to handle */\n        if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {\n          if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {\n            return false;\n          }\n          if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {\n            return false;\n          }\n        }\n\n        /* Keep content except for bad-listed elements */\n        if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {\n          const parentNode = getParentNode(currentNode) || currentNode.parentNode;\n          const childNodes = getChildNodes(currentNode) || currentNode.childNodes;\n          if (childNodes && parentNode) {\n            const childCount = childNodes.length;\n            for (let i = childCount - 1; i >= 0; --i) {\n              const childClone = cloneNode(childNodes[i], true);\n              childClone.__removalCount = (currentNode.__removalCount || 0) + 1;\n              parentNode.insertBefore(childClone, getNextSibling(currentNode));\n            }\n          }\n        }\n        _forceRemove(currentNode);\n        return true;\n      }\n\n      /* Check whether element has a valid namespace */\n      if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) {\n        _forceRemove(currentNode);\n        return true;\n      }\n\n      /* Make sure that older browsers don't get fallback-tag mXSS */\n      if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(/<\\/no(script|embed|frames)/i, currentNode.innerHTML)) {\n        _forceRemove(currentNode);\n        return true;\n      }\n\n      /* Sanitize element content to be template-safe */\n      if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) {\n        /* Get the element's text content */\n        content = currentNode.textContent;\n        arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {\n          content = stringReplace(content, expr, ' ');\n        });\n        if (currentNode.textContent !== content) {\n          arrayPush(DOMPurify.removed, {\n            element: currentNode.cloneNode()\n          });\n          currentNode.textContent = content;\n        }\n      }\n\n      /* Execute a hook if present */\n      _executeHook('afterSanitizeElements', currentNode, null);\n      return false;\n    };\n\n    /**\n     * _isValidAttribute\n     *\n     * @param  {string} lcTag Lowercase tag name of containing element.\n     * @param  {string} lcName Lowercase attribute name.\n     * @param  {string} value Attribute value.\n     * @return {Boolean} Returns true if `value` is valid, otherwise false.\n     */\n    // eslint-disable-next-line complexity\n    const _isValidAttribute = function _isValidAttribute(lcTag, lcName, value) {\n      /* Make sure attribute cannot clobber */\n      if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) {\n        return false;\n      }\n\n      /* Allow valid data-* attributes: At least one character after \"-\"\n          (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)\n          XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)\n          We don't need to check the value; it's always URI safe. */\n      if (ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) ; else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) {\n        if (\n        // First condition does a very basic check if a) it's basically a valid custom element tagname AND\n        // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck\n        // and c) if the attribute name passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.attributeNameCheck\n        _isBasicCustomElement(lcTag) && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(lcTag)) && (CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName) || CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.attributeNameCheck(lcName)) ||\n        // Alternative, second condition checks if it's an `is`-attribute, AND\n        // the value passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck\n        lcName === 'is' && CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(value))) ; else {\n          return false;\n        }\n        /* Check value is safe. First, is attr inert? If so, is safe */\n      } else if (URI_SAFE_ATTRIBUTES[lcName]) ; else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if ((lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') && lcTag !== 'script' && stringIndexOf(value, 'data:') === 0 && DATA_URI_TAGS[lcTag]) ; else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if (value) {\n        return false;\n      } else ;\n      return true;\n    };\n\n    /**\n     * _isBasicCustomElement\n     * checks if at least one dash is included in tagName, and it's not the first char\n     * for more sophisticated checking see https://github.com/sindresorhus/validate-element-name\n     *\n     * @param {string} tagName name of the tag of the node to sanitize\n     * @returns {boolean} Returns true if the tag name meets the basic criteria for a custom element, otherwise false.\n     */\n    const _isBasicCustomElement = function _isBasicCustomElement(tagName) {\n      return tagName !== 'annotation-xml' && stringMatch(tagName, CUSTOM_ELEMENT);\n    };\n\n    /**\n     * _sanitizeAttributes\n     *\n     * @protect attributes\n     * @protect nodeName\n     * @protect removeAttribute\n     * @protect setAttribute\n     *\n     * @param  {Node} currentNode to sanitize\n     */\n    const _sanitizeAttributes = function _sanitizeAttributes(currentNode) {\n      /* Execute a hook if present */\n      _executeHook('beforeSanitizeAttributes', currentNode, null);\n      const {\n        attributes\n      } = currentNode;\n\n      /* Check if we have attributes; if not we might have a text node */\n      if (!attributes) {\n        return;\n      }\n      const hookEvent = {\n        attrName: '',\n        attrValue: '',\n        keepAttr: true,\n        allowedAttributes: ALLOWED_ATTR\n      };\n      let l = attributes.length;\n\n      /* Go backwards over all attributes; safely remove bad ones */\n      while (l--) {\n        const attr = attributes[l];\n        const {\n          name,\n          namespaceURI,\n          value: attrValue\n        } = attr;\n        const lcName = transformCaseFunc(name);\n        let value = name === 'value' ? attrValue : stringTrim(attrValue);\n\n        /* Execute a hook if present */\n        hookEvent.attrName = lcName;\n        hookEvent.attrValue = value;\n        hookEvent.keepAttr = true;\n        hookEvent.forceKeepAttr = undefined; // Allows developers to see this is a property they can set\n        _executeHook('uponSanitizeAttribute', currentNode, hookEvent);\n        value = hookEvent.attrValue;\n        /* Did the hooks approve of the attribute? */\n        if (hookEvent.forceKeepAttr) {\n          continue;\n        }\n\n        /* Remove attribute */\n        _removeAttribute(name, currentNode);\n\n        /* Did the hooks approve of the attribute? */\n        if (!hookEvent.keepAttr) {\n          continue;\n        }\n\n        /* Work around a security issue in jQuery 3.0 */\n        if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\\/>/i, value)) {\n          _removeAttribute(name, currentNode);\n          continue;\n        }\n\n        /* Work around a security issue with comments inside attributes */\n        if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\\/(style|title)/i, value)) {\n          _removeAttribute(name, currentNode);\n          continue;\n        }\n\n        /* Sanitize attribute content to be template-safe */\n        if (SAFE_FOR_TEMPLATES) {\n          arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {\n            value = stringReplace(value, expr, ' ');\n          });\n        }\n\n        /* Is `value` valid for this attribute? */\n        const lcTag = transformCaseFunc(currentNode.nodeName);\n        if (!_isValidAttribute(lcTag, lcName, value)) {\n          continue;\n        }\n\n        /* Full DOM Clobbering protection via namespace isolation,\n         * Prefix id and name attributes with `user-content-`\n         */\n        if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name')) {\n          // Remove the attribute with this value\n          _removeAttribute(name, currentNode);\n\n          // Prefix the value and later re-create the attribute with the sanitized value\n          value = SANITIZE_NAMED_PROPS_PREFIX + value;\n        }\n\n        /* Handle attributes that require Trusted Types */\n        if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function') {\n          if (namespaceURI) ; else {\n            switch (trustedTypes.getAttributeType(lcTag, lcName)) {\n              case 'TrustedHTML':\n                {\n                  value = trustedTypesPolicy.createHTML(value);\n                  break;\n                }\n              case 'TrustedScriptURL':\n                {\n                  value = trustedTypesPolicy.createScriptURL(value);\n                  break;\n                }\n            }\n          }\n        }\n\n        /* Handle invalid data-* attribute set by try-catching it */\n        try {\n          if (namespaceURI) {\n            currentNode.setAttributeNS(namespaceURI, name, value);\n          } else {\n            /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. \"x-schema\". */\n            currentNode.setAttribute(name, value);\n          }\n          if (_isClobbered(currentNode)) {\n            _forceRemove(currentNode);\n          } else {\n            arrayPop(DOMPurify.removed);\n          }\n        } catch (_) {}\n      }\n\n      /* Execute a hook if present */\n      _executeHook('afterSanitizeAttributes', currentNode, null);\n    };\n\n    /**\n     * _sanitizeShadowDOM\n     *\n     * @param  {DocumentFragment} fragment to iterate over recursively\n     */\n    const _sanitizeShadowDOM = function _sanitizeShadowDOM(fragment) {\n      let shadowNode = null;\n      const shadowIterator = _createNodeIterator(fragment);\n\n      /* Execute a hook if present */\n      _executeHook('beforeSanitizeShadowDOM', fragment, null);\n      while (shadowNode = shadowIterator.nextNode()) {\n        /* Execute a hook if present */\n        _executeHook('uponSanitizeShadowNode', shadowNode, null);\n\n        /* Sanitize tags and elements */\n        if (_sanitizeElements(shadowNode)) {\n          continue;\n        }\n\n        /* Deep shadow DOM detected */\n        if (shadowNode.content instanceof DocumentFragment) {\n          _sanitizeShadowDOM(shadowNode.content);\n        }\n\n        /* Check attributes, sanitize if necessary */\n        _sanitizeAttributes(shadowNode);\n      }\n\n      /* Execute a hook if present */\n      _executeHook('afterSanitizeShadowDOM', fragment, null);\n    };\n\n    /**\n     * Sanitize\n     * Public method providing core sanitation functionality\n     *\n     * @param {String|Node} dirty string or DOM node\n     * @param {Object} cfg object\n     */\n    // eslint-disable-next-line complexity\n    DOMPurify.sanitize = function (dirty) {\n      let cfg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};\n      let body = null;\n      let importedNode = null;\n      let currentNode = null;\n      let returnNode = null;\n      /* Make sure we have a string to sanitize.\n        DO NOT return early, as this will return the wrong type if\n        the user has requested a DOM object rather than a string */\n      IS_EMPTY_INPUT = !dirty;\n      if (IS_EMPTY_INPUT) {\n        dirty = '<!-->';\n      }\n\n      /* Stringify, in case dirty is an object */\n      if (typeof dirty !== 'string' && !_isNode(dirty)) {\n        if (typeof dirty.toString === 'function') {\n          dirty = dirty.toString();\n          if (typeof dirty !== 'string') {\n            throw typeErrorCreate('dirty is not a string, aborting');\n          }\n        } else {\n          throw typeErrorCreate('toString is not a function');\n        }\n      }\n\n      /* Return dirty HTML if DOMPurify cannot run */\n      if (!DOMPurify.isSupported) {\n        return dirty;\n      }\n\n      /* Assign config vars */\n      if (!SET_CONFIG) {\n        _parseConfig(cfg);\n      }\n\n      /* Clean up removed elements */\n      DOMPurify.removed = [];\n\n      /* Check if dirty is correctly typed for IN_PLACE */\n      if (typeof dirty === 'string') {\n        IN_PLACE = false;\n      }\n      if (IN_PLACE) {\n        /* Do some early pre-sanitization to avoid unsafe root nodes */\n        if (dirty.nodeName) {\n          const tagName = transformCaseFunc(dirty.nodeName);\n          if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {\n            throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');\n          }\n        }\n      } else if (dirty instanceof Node) {\n        /* If dirty is a DOM element, append to an empty document to avoid\n           elements being stripped by the parser */\n        body = _initDocument('<!---->');\n        importedNode = body.ownerDocument.importNode(dirty, true);\n        if (importedNode.nodeType === NODE_TYPE.element && importedNode.nodeName === 'BODY') {\n          /* Node is already a body, use as is */\n          body = importedNode;\n        } else if (importedNode.nodeName === 'HTML') {\n          body = importedNode;\n        } else {\n          // eslint-disable-next-line unicorn/prefer-dom-node-append\n          body.appendChild(importedNode);\n        }\n      } else {\n        /* Exit directly if we have nothing to do */\n        if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT &&\n        // eslint-disable-next-line unicorn/prefer-includes\n        dirty.indexOf('<') === -1) {\n          return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty;\n        }\n\n        /* Initialize the document to work on */\n        body = _initDocument(dirty);\n\n        /* Check we have a DOM node from the data */\n        if (!body) {\n          return RETURN_DOM ? null : RETURN_TRUSTED_TYPE ? emptyHTML : '';\n        }\n      }\n\n      /* Remove first element node (ours) if FORCE_BODY is set */\n      if (body && FORCE_BODY) {\n        _forceRemove(body.firstChild);\n      }\n\n      /* Get node iterator */\n      const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body);\n\n      /* Now start iterating over the created document */\n      while (currentNode = nodeIterator.nextNode()) {\n        /* Sanitize tags and elements */\n        if (_sanitizeElements(currentNode)) {\n          continue;\n        }\n\n        /* Shadow DOM detected, sanitize it */\n        if (currentNode.content instanceof DocumentFragment) {\n          _sanitizeShadowDOM(currentNode.content);\n        }\n\n        /* Check attributes, sanitize if necessary */\n        _sanitizeAttributes(currentNode);\n      }\n\n      /* If we sanitized `dirty` in-place, return it. */\n      if (IN_PLACE) {\n        return dirty;\n      }\n\n      /* Return sanitized string or DOM */\n      if (RETURN_DOM) {\n        if (RETURN_DOM_FRAGMENT) {\n          returnNode = createDocumentFragment.call(body.ownerDocument);\n          while (body.firstChild) {\n            // eslint-disable-next-line unicorn/prefer-dom-node-append\n            returnNode.appendChild(body.firstChild);\n          }\n        } else {\n          returnNode = body;\n        }\n        if (ALLOWED_ATTR.shadowroot || ALLOWED_ATTR.shadowrootmode) {\n          /*\n            AdoptNode() is not used because internal state is not reset\n            (e.g. the past names map of a HTMLFormElement), this is safe\n            in theory but we would rather not risk another attack vector.\n            The state that is cloned by importNode() is explicitly defined\n            by the specs.\n          */\n          returnNode = importNode.call(originalDocument, returnNode, true);\n        }\n        return returnNode;\n      }\n      let serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML;\n\n      /* Serialize doctype if allowed */\n      if (WHOLE_DOCUMENT && ALLOWED_TAGS['!doctype'] && body.ownerDocument && body.ownerDocument.doctype && body.ownerDocument.doctype.name && regExpTest(DOCTYPE_NAME, body.ownerDocument.doctype.name)) {\n        serializedHTML = '<!DOCTYPE ' + body.ownerDocument.doctype.name + '>\\n' + serializedHTML;\n      }\n\n      /* Sanitize final string template-safe */\n      if (SAFE_FOR_TEMPLATES) {\n        arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {\n          serializedHTML = stringReplace(serializedHTML, expr, ' ');\n        });\n      }\n      return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML;\n    };\n\n    /**\n     * Public method to set the configuration once\n     * setConfig\n     *\n     * @param {Object} cfg configuration object\n     */\n    DOMPurify.setConfig = function () {\n      let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n      _parseConfig(cfg);\n      SET_CONFIG = true;\n    };\n\n    /**\n     * Public method to remove the configuration\n     * clearConfig\n     *\n     */\n    DOMPurify.clearConfig = function () {\n      CONFIG = null;\n      SET_CONFIG = false;\n    };\n\n    /**\n     * Public method to check if an attribute value is valid.\n     * Uses last set config, if any. Otherwise, uses config defaults.\n     * isValidAttribute\n     *\n     * @param  {String} tag Tag name of containing element.\n     * @param  {String} attr Attribute name.\n     * @param  {String} value Attribute value.\n     * @return {Boolean} Returns true if `value` is valid. Otherwise, returns false.\n     */\n    DOMPurify.isValidAttribute = function (tag, attr, value) {\n      /* Initialize shared config vars if necessary. */\n      if (!CONFIG) {\n        _parseConfig({});\n      }\n      const lcTag = transformCaseFunc(tag);\n      const lcName = transformCaseFunc(attr);\n      return _isValidAttribute(lcTag, lcName, value);\n    };\n\n    /**\n     * AddHook\n     * Public method to add DOMPurify hooks\n     *\n     * @param {String} entryPoint entry point for the hook to add\n     * @param {Function} hookFunction function to execute\n     */\n    DOMPurify.addHook = function (entryPoint, hookFunction) {\n      if (typeof hookFunction !== 'function') {\n        return;\n      }\n      hooks[entryPoint] = hooks[entryPoint] || [];\n      arrayPush(hooks[entryPoint], hookFunction);\n    };\n\n    /**\n     * RemoveHook\n     * Public method to remove a DOMPurify hook at a given entryPoint\n     * (pops it from the stack of hooks if more are present)\n     *\n     * @param {String} entryPoint entry point for the hook to remove\n     * @return {Function} removed(popped) hook\n     */\n    DOMPurify.removeHook = function (entryPoint) {\n      if (hooks[entryPoint]) {\n        return arrayPop(hooks[entryPoint]);\n      }\n    };\n\n    /**\n     * RemoveHooks\n     * Public method to remove all DOMPurify hooks at a given entryPoint\n     *\n     * @param  {String} entryPoint entry point for the hooks to remove\n     */\n    DOMPurify.removeHooks = function (entryPoint) {\n      if (hooks[entryPoint]) {\n        hooks[entryPoint] = [];\n      }\n    };\n\n    /**\n     * RemoveAllHooks\n     * Public method to remove all DOMPurify hooks\n     */\n    DOMPurify.removeAllHooks = function () {\n      hooks = {};\n    };\n    return DOMPurify;\n  }\n  var purify = createDOMPurify();\n\n  return purify;\n\n}));\n//# sourceMappingURL=purify.js.map\n", "import { user } from \"@web/core/user\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { useSetupAction } from \"@web/search/action_hook\";\nimport { SEARCH_KEYS } from \"@web/search/with_search/with_search\";\nimport { buildSampleORM } from \"./sample_server\";\n\nimport { EventBus, onWillStart, onWillUpdateProps, status, useComponent } from \"@odoo/owl\";\n\n/**\n * @typedef {import(\"@web/search/search_model\").SearchParams} SearchParams\n */\n\nexport class Model {\n    /**\n     * @param {Object} env\n     * @param {Object} services\n     */\n    constructor(env, params, services) {\n        this.env = env;\n        this.orm = services.orm;\n        this.bus = new EventBus();\n        this.setup(params, services);\n    }\n\n    /**\n     * @param {Object} params\n     * @param {Object} services\n     */\n    setup(/* params, services */) {}\n\n    /**\n     * @param {SearchParams} searchParams\n     */\n    async load(/* searchParams */) {}\n\n    /**\n     * This function is meant to be overriden by models that want to implement\n     * the sample data feature. It should return true iff the last loaded state\n     * actually contains data. If not, another load will be done (if the sample\n     * feature is enabled) with the orm service substituted by another using the\n     * SampleServer, to have sample data to display instead of an empty screen.\n     *\n     * @returns {boolean}\n     */\n    hasData() {\n        return true;\n    }\n\n    /**\n     * This function is meant to be overriden by models that want to combine\n     * sample data with real groups that exist on the server.\n     *\n     * @returns {boolean}\n     */\n    getGroups() {\n        return null;\n    }\n\n    notify() {\n        this.bus.trigger(\"update\");\n    }\n}\nModel.services = [];\n\n/**\n * @param {Object} props\n * @returns {SearchParams}\n */\nfunction getSearchParams(props) {\n    const params = {};\n    for (const key of SEARCH_KEYS) {\n        params[key] = props[key];\n    }\n    return params;\n}\n\n/**\n * @template {typeof Model} T\n * @param {T} ModelClass\n * @param {Object} params\n * @param {Object} [options]\n * @param {Function} [options.beforeFirstLoad]\n * @returns {InstanceType<T>}\n */\nexport function useModel(ModelClass, params, options = {}) {\n    const component = useComponent();\n    const services = {};\n    for (const key of ModelClass.services) {\n        services[key] = useService(key);\n    }\n    services.orm = services.orm || useService(\"orm\");\n    const model = new ModelClass(component.env, params, services);\n    onWillStart(async () => {\n        await options.beforeFirstLoad?.();\n        return model.load(component.props);\n    });\n    onWillUpdateProps((nextProps) => model.load(nextProps));\n    return model;\n}\n\n/**\n * @template {typeof Model} T\n * @param {T} ModelClass\n * @param {Object} params\n * @param {Object} [options]\n * @param {Function} [options.onUpdate]\n * @param {Function} [options.onWillStart]\n * @param {Function} [options.onWillStartAfterLoad]\n * @returns {InstanceType<T>}\n */\nexport function useModelWithSampleData(ModelClass, params, options = {}) {\n    const component = useComponent();\n    if (!(ModelClass.prototype instanceof Model)) {\n        throw new Error(`the model class should extend Model`);\n    }\n    const services = {};\n    for (const key of ModelClass.services) {\n        services[key] = useService(key);\n    }\n    services.orm = services.orm || useService(\"orm\");\n\n    if (!(\"isAlive\" in params)) {\n        params.isAlive = () => status(component) !== \"destroyed\";\n    }\n\n    const model = new ModelClass(component.env, params, services);\n\n    useBus(\n        model.bus,\n        \"update\",\n        options.onUpdate ||\n            (() => {\n                component.render(true); // FIXME WOWL reactivity\n            })\n    );\n\n    const globalState = component.props.globalState || {};\n    const localState = component.props.state || {};\n    let useSampleModel =\n        component.props.useSampleModel &&\n        (!(\"useSampleModel\" in globalState) || globalState.useSampleModel);\n    model.useSampleModel = useSampleModel;\n    const orm = model.orm;\n    let sampleORM = localState.sampleORM;\n    let started = false;\n\n    async function load(props) {\n        const searchParams = getSearchParams(props);\n        await model.load(searchParams);\n        if (useSampleModel && !model.hasData()) {\n            sampleORM =\n                sampleORM || buildSampleORM(component.props.resModel, component.props.fields, user);\n            // Load data with sampleORM then restore real ORM.\n            model.orm = sampleORM;\n            await model.load(searchParams);\n            model.orm = orm;\n        } else {\n            useSampleModel = false;\n            model.useSampleModel = useSampleModel;\n        }\n        if (started) {\n            model.notify();\n        }\n    }\n    onWillStart(async () => {\n        if (options.onWillStart) {\n            await options.onWillStart();\n        }\n        await load(component.props);\n        if (options.onWillStartAfterLoad) {\n            await options.onWillStartAfterLoad();\n        }\n        started = true;\n    });\n    onWillUpdateProps((nextProps) => {\n        useSampleModel = false;\n        load(nextProps);\n    });\n\n    useSetupAction({\n        getGlobalState() {\n            if (component.props.useSampleModel) {\n                return { useSampleModel };\n            }\n        },\n        getLocalState: () => {\n            return { sampleORM };\n        },\n    });\n\n    return model;\n}\n\nexport function _makeFieldFromPropertyDefinition(name, definition, relatedPropertyField) {\n    return {\n        ...definition,\n        name,\n        propertyName: definition.name,\n        relation: definition.comodel,\n        relatedPropertyField,\n    };\n}\n\nexport async function addPropertyFieldDefs(orm, resModel, context, fields, groupBy) {\n    const proms = [];\n    for (const gb of groupBy) {\n        if (gb in fields) {\n            continue;\n        }\n        const [fieldName] = gb.split(\".\");\n        const field = fields[fieldName];\n        if (field?.type === \"properties\") {\n            proms.push(\n                orm\n                    .call(resModel, \"get_property_definition\", [gb], {\n                        context,\n                    })\n                    .then((definition) => {\n                        fields[gb] = _makeFieldFromPropertyDefinition(gb, definition, field);\n                    })\n                    .catch(() => {\n                        fields[gb] = _makeFieldFromPropertyDefinition(gb, {}, field);\n                    })\n            );\n        }\n    }\n    return Promise.all(proms);\n}\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { pick } from \"@web/core/utils/objects\";\nimport { RelationalModel } from \"@web/model/relational_model/relational_model\";\nimport { getFieldsSpec } from \"@web/model/relational_model/utils\";\nimport { Component, xml, onWillStart, onWillUpdateProps, useState } from \"@odoo/owl\";\n\nconst defaultActiveField = { attrs: {}, options: {}, domain: \"[]\", string: \"\" };\n\nclass StandaloneRelationalModel extends RelationalModel {\n    load(params = {}) {\n        if (params.values) {\n            const data = params.values;\n            const config = this._getNextConfig(this.config, params);\n            this.root = this._createRoot(config, data);\n            this.config = config;\n            return;\n        }\n        return super.load(params);\n    }\n}\n\nclass _Record extends Component {\n    static template = xml`<t t-slot=\"default\" record=\"model.root\"/>`;\n    static props = [\"slots\", \"info\", \"fields\", \"values?\"];\n    setup() {\n        this.orm = useService(\"orm\");\n        const resModel = this.props.info.resModel;\n        const activeFields = this.getActiveFields();\n        const modelParams = {\n            config: {\n                resModel,\n                fields: this.props.fields,\n                isMonoRecord: true,\n                activeFields,\n                resId: this.props.info.resId,\n                mode: this.props.info.mode,\n            },\n            hooks: {\n                onRecordSaved: this.props.info.onRecordSaved || (() => {}),\n                onWillSaveRecord: this.props.info.onWillSaveRecord || (() => {}),\n                onRecordChanged: this.props.info.onRecordChanged || (() => {}),\n            },\n        };\n        const modelServices = Object.fromEntries(\n            StandaloneRelationalModel.services.map((servName) => {\n                return [servName, useService(servName)];\n            })\n        );\n        modelServices.orm = this.orm;\n        this.model = useState(new StandaloneRelationalModel(this.env, modelParams, modelServices));\n\n        const prepareLoadWithValues = async (values) => {\n            values = pick(values, ...Object.keys(modelParams.config.activeFields));\n            const proms = [];\n            for (const fieldName in values) {\n                if ([\"one2many\", \"many2many\"].includes(this.props.fields[fieldName].type)) {\n                    if (values[fieldName].length && typeof values[fieldName][0] === \"number\") {\n                        const resModel = this.props.fields[fieldName].relation;\n                        const resIds = values[fieldName];\n                        const activeField = modelParams.config.activeFields[fieldName];\n                        if (activeField.related) {\n                            const { activeFields, fields } = activeField.related;\n                            const fieldSpec = getFieldsSpec(activeFields, fields, {});\n                            const kwargs = {\n                                context: activeField.context || {},\n                                specification: fieldSpec,\n                            };\n                            proms.push(\n                                this.orm.webRead(resModel, resIds, kwargs).then((records) => {\n                                    values[fieldName] = records;\n                                })\n                            );\n                        }\n                    }\n                }\n                if (this.props.fields[fieldName].type === \"many2one\") {\n                    const loadDisplayName = async (resId) => {\n                        const resModel = this.props.fields[fieldName].relation;\n                        const activeField = modelParams.config.activeFields[fieldName];\n                        const kwargs = {\n                            context: activeField.context || {},\n                            specification: { display_name: {} },\n                        };\n                        const records = await this.orm.webRead(resModel, [resId], kwargs);\n                        return records[0].display_name;\n                    };\n                    if (typeof values[fieldName] === \"number\") {\n                        const prom = loadDisplayName(values[fieldName]);\n                        prom.then((displayName) => {\n                            values[fieldName] = {\n                                id: values[fieldName],\n                                display_name: displayName,\n                            };\n                        });\n                        proms.push(prom);\n                    } else if (Array.isArray(values[fieldName])) {\n                        if (values[fieldName][1] === undefined) {\n                            const prom = loadDisplayName(values[fieldName][0]);\n                            prom.then((displayName) => {\n                                values[fieldName] = {\n                                    id: values[fieldName][0],\n                                    display_name: displayName,\n                                };\n                            });\n                            proms.push(prom);\n                        }\n                        values[fieldName] = {\n                            id: values[fieldName][0],\n                            display_name: values[fieldName][1],\n                        };\n                    }\n                }\n                await Promise.all(proms);\n            }\n            return values;\n        };\n        onWillStart(async () => {\n            if (this.props.values) {\n                const values = await prepareLoadWithValues(this.props.values);\n                return this.model.load({ values });\n            } else {\n                return this.model.load();\n            }\n        });\n        onWillUpdateProps(async (nextProps) => {\n            const params = {};\n            if (nextProps.info.resId !== this.model.root.resId) {\n                params.resId = nextProps.info.resId;\n            }\n            if (nextProps.values) {\n                params.values = await prepareLoadWithValues(nextProps.values);\n            }\n            if (Object.keys(params).length) {\n                return this.model.load(params);\n            }\n        });\n    }\n\n    getActiveFields() {\n        if (this.props.info.activeFields) {\n            const activeFields = {};\n            for (const [fName, fInfo] of Object.entries(this.props.info.activeFields)) {\n                activeFields[fName] = { ...defaultActiveField, ...fInfo };\n            }\n            return activeFields;\n        }\n        return Object.fromEntries(\n            this.props.info.fieldNames.map((f) => [f, { ...defaultActiveField }])\n        );\n    }\n}\n\nexport class Record extends Component {\n    static template = xml`<_Record fields=\"fields\" slots=\"props.slots\" values=\"props.values\" info=\"props\" />`;\n    static components = { _Record };\n    static props = [\n        \"slots\",\n        \"resModel?\",\n        \"fieldNames?\",\n        \"activeFields?\",\n        \"fields?\",\n        \"resId?\",\n        \"mode?\",\n        \"values?\",\n        \"onRecordChanged?\",\n        \"onRecordSaved?\",\n        \"onWillSaveRecord?\",\n    ];\n    setup() {\n        if (this.props.fields) {\n            this.fields = this.props.fields;\n        } else {\n            const orm = useService(\"orm\");\n            onWillStart(async () => {\n                this.fields = await orm.call(\n                    this.props.resModel,\n                    \"fields_get\",\n                    [this.props.fieldNames],\n                    {}\n                );\n            });\n        }\n    }\n}\n", "import { markRaw } from \"@odoo/owl\";\nimport { evalDomain } from \"@web/core/domain\";\nimport { Reactive } from \"@web/core/utils/reactive\";\nimport { getId } from \"./utils\";\n\n/**\n * @typedef Params\n * @property {string} resModel\n * @property {Object} context\n * @property {{[key: string]: FieldInfo}} activeFields\n * @property {{[key: string]: Field}} fields\n */\n\n/**\n * @typedef Field\n * @property {string} name\n * @property {string} type\n * @property {[string,string][]} [selection]\n */\n\n/**\n * @typedef FieldInfo\n * @property {string} context\n * @property {boolean} invisible\n * @property {boolean} readonly\n * @property {boolean} required\n * @property {boolean} onChange\n */\n\nexport class DataPoint extends Reactive {\n    /**\n     * @param {import(\"./relational_model\").RelationalModel} model\n     * @param {import(\"./relational_model\").Config\"} config\n     * @param {any} data\n     * @param {Object} [options]\n     */\n    constructor(model, config, data, options) {\n        super(...arguments);\n        this.id = getId(\"datapoint\");\n        this.model = model;\n        markRaw(config.activeFields);\n        markRaw(config.fields);\n        this._config = config;\n        this.setup(config, data, options);\n    }\n\n    /**\n     * @abstract\n     * @param {Object} params\n     * @param {Object} state\n     */\n    setup() {}\n\n    get activeFields() {\n        return this.config.activeFields;\n    }\n\n    get fields() {\n        return this.config.fields;\n    }\n\n    get fieldNames() {\n        return Object.keys(this.activeFields).filter(\n            (fieldName) => !this.fields[fieldName].relatedPropertyField\n        );\n    }\n\n    get resModel() {\n        return this.config.resModel;\n    }\n\n    get config() {\n        return this._config;\n    }\n\n    get context() {\n        return this.config.context;\n    }\n\n    get currentCompanyId() {\n        return this.config.currentCompanyId;\n    }\n\n    // -------------------------------------------------------------------------\n    // Public\n    // -------------------------------------------------------------------------\n\n    /**\n     * @param {string} fieldName\n     * @returns {boolean}\n     */\n    isFieldReadonly(fieldName) {\n        const activeField = this.activeFields[fieldName];\n        const { readonly } = activeField || this.fields[fieldName];\n        return readonly && evalDomain(readonly, this.evalContext);\n    }\n}\n", "//@ts-check\n\nimport { Domain } from \"@web/core/domain\";\nimport { DynamicList } from \"./dynamic_list\";\nimport { getGroupServerValue } from \"./utils\";\n\nexport class DynamicGroupList extends DynamicList {\n    static type = \"DynamicGroupList\";\n\n    /**\n     * @param {import(\"./relational_model\").Config} config\n     * @param {Object} data\n     */\n    setup(config, data) {\n        super.setup(...arguments);\n        this.isGrouped = true;\n        this._nbRecordsMatchingDomain = null;\n        this._setData(data);\n    }\n\n    _setData(data) {\n        /** @type {import(\"./group\").Group[]} */\n        this.groups = data.groups.map((g) => this._createGroupDatapoint(g));\n        this.count = data.length;\n        this._selectDomain(this.isDomainSelected);\n    }\n\n    // -------------------------------------------------------------------------\n    // Getters\n    // -------------------------------------------------------------------------\n\n    get groupBy() {\n        return this.config.groupBy;\n    }\n\n    get groupByField() {\n        return this.fields[this.groupBy[0].split(\":\")[0]];\n    }\n\n    get hasData() {\n        return this.groups.some((group) => group.hasData);\n    }\n\n    get isRecordCountTrustable() {\n        return this.count <= this.limit || this._nbRecordsMatchingDomain !== null;\n    }\n\n    /**\n     * List of loaded records inside groups.\n     * @returns {import(\"./record\").Record[]}\n     */\n    get records() {\n        return this.groups\n            .filter((group) => !group.isFolded)\n            .map((group) => group.records)\n            .flat();\n    }\n\n    /**\n     * @returns {number}\n     */\n    get recordCount() {\n        if (this._nbRecordsMatchingDomain !== null) {\n            return this._nbRecordsMatchingDomain;\n        }\n        return this.groups.reduce((acc, group) => acc + group.count, 0);\n    }\n\n    // -------------------------------------------------------------------------\n    // Public\n    // -------------------------------------------------------------------------\n\n    /**\n     * @param {string} groupName\n     * @param {string} [foldField] if given, will write true on this field to\n     *   make the group folded by default\n     */\n    async createGroup(groupName, foldField) {\n        if (!this.groupByField || this.groupByField.type !== \"many2one\") {\n            throw new Error(\"Cannot create a group on a non many2one group field\");\n        }\n\n        await this.model.mutex.exec(() => this._createGroup(groupName, foldField));\n    }\n\n    async deleteGroups(groups) {\n        await this.model.mutex.exec(() => this._deleteGroups(groups));\n    }\n\n    /**\n     * @param {string} dataRecordId\n     * @param {string} dataGroupId\n     * @param {string} refId\n     * @param {string} targetGroupId\n     */\n    async moveRecord(dataRecordId, dataGroupId, refId, targetGroupId) {\n        const targetGroup = this.groups.find((g) => g.id === targetGroupId);\n        if (dataGroupId === targetGroupId) {\n            // move a record inside the same group\n            await targetGroup.list._resequence(\n                targetGroup.list.records,\n                this.resModel,\n                dataRecordId,\n                refId\n            );\n            return;\n        }\n\n        // move record from a group to another group\n        const sourceGroup = this.groups.find((g) => g.id === dataGroupId);\n        const recordIndex = sourceGroup.list.records.findIndex((r) => r.id === dataRecordId);\n        const record = sourceGroup.list.records[recordIndex];\n        // step 1: move record to correct position\n        const refIndex = targetGroup.list.records.findIndex((r) => r.id === refId);\n        const oldIndex = sourceGroup.list.records.findIndex((r) => r.id === dataRecordId);\n\n        const sourceList = sourceGroup.list;\n        // if the source contains more records than what's loaded, reload it after moving the record\n        const mustReloadSourceList = sourceList.count > sourceList.offset + sourceList.limit;\n\n        sourceGroup._removeRecords([record.id]);\n        targetGroup._addRecord(record, refIndex + 1);\n        // step 2: update record value\n        const value =\n            targetGroup.groupByField.type === \"many2one\"\n                ? [targetGroup.value, targetGroup.displayName]\n                : targetGroup.value;\n        const revert = () => {\n            targetGroup._removeRecords([record.id]);\n            sourceGroup._addRecord(record, oldIndex);\n        };\n        try {\n            const changes = { [targetGroup.groupByField.name]: value };\n            const res = await record.update(changes, { save: true });\n            if (!res) {\n                return revert();\n            }\n        } catch (e) {\n            // revert changes\n            revert();\n            throw e;\n        }\n\n        const proms = [];\n        if (mustReloadSourceList) {\n            const { offset, limit, orderBy, domain } = sourceGroup.list;\n            proms.push(sourceGroup.list._load(offset, limit, orderBy, domain));\n        }\n        if (!targetGroup.isFolded) {\n            const targetList = targetGroup.list;\n            const records = targetList.records;\n            proms.push(targetList._resequence(records, this.resModel, dataRecordId, refId));\n        }\n        return Promise.all(proms);\n    }\n\n    async resequence(movedGroupId, targetGroupId) {\n        if (!this.groupByField || this.groupByField.type !== \"many2one\") {\n            throw new Error(\"Cannot resequence a group on a non many2one group field\");\n        }\n\n        return this.model.mutex.exec(async () => {\n            await this._resequence(\n                this.groups,\n                this.groupByField.relation,\n                movedGroupId,\n                targetGroupId\n            );\n        });\n    }\n\n    async selectDomain(value) {\n        return this.model.mutex.exec(async () => {\n            await this._ensureCorrectRecordCount();\n            this._selectDomain(value);\n        });\n    }\n\n    async sortBy(fieldName) {\n        if (!this.groups.length) {\n            return;\n        }\n        if (this.groups.every((group) => group.isFolded)) {\n            // all groups are folded\n            if (this.groupByField.name !== fieldName) {\n                // grouped by another field than fieldName\n                if (!(fieldName in this.groups[0].aggregates)) {\n                    // fieldName has no aggregate values\n                    return;\n                }\n            }\n        }\n        return super.sortBy(fieldName);\n    }\n\n    // -------------------------------------------------------------------------\n    // Protected\n    // -------------------------------------------------------------------------\n\n    async _createGroup(groupName, foldField = false) {\n        const [id] = await this.model.orm.call(\n            this.groupByField.relation,\n            \"name_create\",\n            [groupName],\n            { context: this.context }\n        );\n        if (foldField) {\n            await this.model.orm.write(\n                this.groupByField.relation,\n                [id],\n                { [foldField]: true },\n                { context: this.context }\n            );\n        }\n        const lastGroup = this.groups.at(-1);\n\n        // This is almost a copy/past of the code in relational_model.js\n        // Maybe we can create an addGroup method in relational_model.js\n        // and call it from here and from relational_model.js\n        const commonConfig = {\n            resModel: this.config.resModel,\n            fields: this.config.fields,\n            activeFields: this.config.activeFields,\n        };\n        const context = {\n            ...this.context,\n            [`default_${this.groupByField.name}`]: id,\n        };\n        const nextConfigGroups = { ...this.config.groups };\n        const domain = Domain.and([this.domain, [[this.groupByField.name, \"=\", id]]]).toList();\n        nextConfigGroups[id] = {\n            ...commonConfig,\n            context,\n            groupByFieldName: this.groupByField.name,\n            isFolded: Boolean(foldField),\n            initialDomain: domain,\n            list: {\n                ...commonConfig,\n                context,\n                domain: domain,\n                groupBy: [],\n                orderBy: this.orderBy,\n            },\n        };\n        this.model._updateConfig(this.config, { groups: nextConfigGroups }, { reload: false });\n\n        const data = {\n            count: 0,\n            length: 0,\n            records: [],\n            __domain: domain,\n            [this.groupByField.name]: [id, groupName],\n            value: id,\n            serverValue: getGroupServerValue(this.groupByField, id),\n            displayName: groupName,\n            rawValue: [id, groupName],\n        };\n\n        const group = this._createGroupDatapoint(data);\n        if (lastGroup) {\n            const groups = [...this.groups, group];\n            await this._resequence(groups, this.groupByField.relation, group.id, lastGroup.id);\n            this.groups = groups;\n        } else {\n            this.groups.push(group);\n        }\n    }\n\n    _createGroupDatapoint(data) {\n        return new this.model.constructor.Group(this.model, this.config.groups[data.value], data);\n    }\n\n    async _deleteGroups(groups) {\n        const shouldReload = groups.some((g) => g.count > 0);\n        await this._unlinkGroups(groups);\n        const configGroups = { ...this.config.groups };\n        for (const group of groups) {\n            delete configGroups[group.value];\n        }\n        if (shouldReload) {\n            await this.model._updateConfig(\n                this.config,\n                { groups: configGroups },\n                { commit: this._setData.bind(this) }\n            );\n        } else {\n            for (const group of groups) {\n                this._removeGroup(group);\n            }\n            this.model._updateConfig(this.config, { groups: configGroups }, { reload: false });\n        }\n    }\n\n    async _ensureCorrectRecordCount() {\n        if (!this.isRecordCountTrustable) {\n            this._nbRecordsMatchingDomain = await this.model.orm.searchCount(\n                this.resModel,\n                this.domain,\n                { limit: this.model.initialCountLimit }\n            );\n        }\n    }\n\n    _getDPresId(group) {\n        return group.value;\n    }\n\n    _getDPFieldValue(group, handleField) {\n        return group[handleField];\n    }\n\n    async _load(offset, limit, orderBy, domain) {\n        await this.model._updateConfig(\n            this.config,\n            { offset, limit, orderBy, domain },\n            { commit: this._setData.bind(this) }\n        );\n        if (this.isDomainSelected) {\n            await this._ensureCorrectRecordCount();\n        }\n    }\n\n    _removeGroup(group) {\n        const index = this.groups.findIndex((g) => g.id === group.id);\n        this.groups.splice(index, 1);\n        this.count--;\n    }\n\n    _removeRecords(recordIds) {\n        const proms = [];\n        for (const group of this.groups) {\n            proms.push(group._removeRecords(recordIds));\n        }\n        return Promise.all(proms);\n    }\n\n    _selectDomain(value) {\n        for (const group of this.groups) {\n            group.list._selectDomain(value);\n        }\n        super._selectDomain(value);\n    }\n\n    async _toggleSelection() {\n        if (!this.records.length) {\n            // all groups are folded, so there's no visible records => select all domain\n            if (!this.isDomainSelected) {\n                await this._ensureCorrectRecordCount();\n                this._selectDomain(true);\n            } else {\n                this._selectDomain(false);\n            }\n        } else {\n            super._toggleSelection();\n        }\n    }\n\n    _unlinkGroups(groups) {\n        const groupResIds = groups.map((g) => g.value);\n        return this.model.orm.unlink(this.groupByField.relation, groupResIds, {\n            context: this.context,\n        });\n    }\n}\n", "import { AlertDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { unique } from \"@web/core/utils/arrays\";\nimport { DataPoint } from \"./datapoint\";\nimport { Record } from \"./record\";\nimport { resequence } from \"./utils\";\n\nconst DEFAULT_HANDLE_FIELD = \"sequence\";\n\nexport class DynamicList extends DataPoint {\n    /**\n     * @param {import(\"./relational_model\").Config} config\n     */\n    setup(config) {\n        super.setup(...arguments);\n        this.handleField = Object.keys(this.activeFields).find(\n            (fieldName) => this.activeFields[fieldName].isHandle\n        );\n        if (!this.handleField && DEFAULT_HANDLE_FIELD in this.fields) {\n            this.handleField = DEFAULT_HANDLE_FIELD;\n        }\n        this.isDomainSelected = false;\n        this.evalContext = this.context;\n    }\n\n    // -------------------------------------------------------------------------\n    // Getters\n    // -------------------------------------------------------------------------\n\n    get groupBy() {\n        return [];\n    }\n\n    get orderBy() {\n        return this.config.orderBy;\n    }\n\n    get domain() {\n        return this.config.domain;\n    }\n\n    /**\n     * Be careful that this getter is costly, as it iterates over the whole list\n     * of records. This property should not be accessed in a loop.\n     */\n    get editedRecord() {\n        return this.records.find((record) => record.isInEdition);\n    }\n\n    get isRecordCountTrustable() {\n        return true;\n    }\n\n    get limit() {\n        return this.config.limit;\n    }\n\n    get offset() {\n        return this.config.offset;\n    }\n\n    /**\n     * Be careful that this getter is costly, as it iterates over the whole list\n     * of records. This property should not be accessed in a loop.\n     */\n    get selection() {\n        return this.records.filter((record) => record.selected);\n    }\n\n    // -------------------------------------------------------------------------\n    // Public\n    // -------------------------------------------------------------------------\n\n    archive(isSelected) {\n        return this.model.mutex.exec(() => this._toggleArchive(isSelected, true));\n    }\n\n    canResequence() {\n        return !!this.handleField;\n    }\n\n    deleteRecords(records = []) {\n        return this.model.mutex.exec(() => this._deleteRecords(records));\n    }\n\n    duplicateRecords(records = []) {\n        return this.model.mutex.exec(() => this._duplicateRecords(records));\n    }\n\n    async enterEditMode(record) {\n        if (this.editedRecord === record) {\n            return true;\n        }\n        const canProceed = await this.leaveEditMode();\n        if (canProceed) {\n            this.model._updateConfig(record.config, { mode: \"edit\" }, { reload: false });\n        }\n        return canProceed;\n    }\n\n    /**\n     * @param {boolean} [isSelected]\n     * @returns {Promise<number[]>}\n     */\n    async getResIds(isSelected) {\n        let resIds;\n        if (isSelected) {\n            if (this.isDomainSelected) {\n                resIds = await this.model.orm.search(this.resModel, this.domain, {\n                    limit: this.model.activeIdsLimit,\n                    context: this.context,\n                });\n            } else {\n                resIds = this.selection.map((r) => r.resId);\n            }\n        } else {\n            resIds = this.records.map((r) => r.resId);\n        }\n        return unique(resIds);\n    }\n\n    async leaveEditMode({ discard } = {}) {\n        let editedRecord = this.editedRecord;\n        if (editedRecord) {\n            let canProceed = true;\n            if (discard) {\n                this._recordToDiscard = editedRecord;\n                await editedRecord.discard();\n                this._recordToDiscard = null;\n                editedRecord = this.editedRecord;\n                if (editedRecord && editedRecord.isNew) {\n                    this._removeRecords([editedRecord.id]);\n                }\n            } else {\n                if (!this.model._urgentSave) {\n                    await editedRecord.checkValidity();\n                    editedRecord = this.editedRecord;\n                    if (!editedRecord) {\n                        return true;\n                    }\n                }\n                if (editedRecord.isNew && !editedRecord.dirty) {\n                    this._removeRecords([editedRecord.id]);\n                } else {\n                    canProceed = await editedRecord.save();\n                }\n            }\n\n            editedRecord = this.editedRecord;\n            if (canProceed && editedRecord) {\n                this.model._updateConfig(\n                    editedRecord.config,\n                    { mode: \"readonly\" },\n                    { reload: false }\n                );\n            } else {\n                return canProceed;\n            }\n        }\n        return true;\n    }\n\n    load(params = {}) {\n        const limit = params.limit === undefined ? this.limit : params.limit;\n        const offset = params.offset === undefined ? this.offset : params.offset;\n        const orderBy = params.orderBy === undefined ? this.orderBy : params.orderBy;\n        const domain = params.domain === undefined ? this.domain : params.domain;\n        return this.model.mutex.exec(() => this._load(offset, limit, orderBy, domain));\n    }\n\n    async multiSave(record) {\n        return this.model.mutex.exec(() => this._multiSave(record));\n    }\n\n    selectDomain(value) {\n        return this.model.mutex.exec(() => this._selectDomain(value));\n    }\n\n    sortBy(fieldName) {\n        return this.model.mutex.exec(() => {\n            let orderBy = [...this.orderBy];\n            if (orderBy.length && orderBy[0].name === fieldName) {\n                orderBy[0] = { name: orderBy[0].name, asc: !orderBy[0].asc };\n            } else {\n                orderBy = orderBy.filter((o) => o.name !== fieldName);\n                orderBy.unshift({\n                    name: fieldName,\n                    asc: true,\n                });\n            }\n            return this._load(this.offset, this.limit, orderBy, this.domain);\n        });\n    }\n\n    toggleSelection() {\n        return this.model.mutex.exec(() => this._toggleSelection());\n    }\n\n    unarchive(isSelected) {\n        return this.model.mutex.exec(() => this._toggleArchive(isSelected, false));\n    }\n\n    // -------------------------------------------------------------------------\n    // Protected\n    // -------------------------------------------------------------------------\n\n    async _duplicateRecords(records) {\n        let resIds;\n        if (records.length) {\n            resIds = unique(records.map((r) => r.resId));\n        } else {\n            resIds = await this.getResIds(true);\n        }\n\n        const duplicated = await this.model.orm.call(this.resModel, \"copy\", [resIds], {\n            context: this.context,\n        });\n        if (resIds.length > duplicated.length) {\n            this.model.notification.add(_t(\"Some records could not be duplicated\"), {\n                title: _t(\"Warning\"),\n            });\n        }\n        return this.model.load();\n    }\n\n    async _deleteRecords(records) {\n        let resIds;\n        if (records.length) {\n            resIds = unique(records.map((r) => r.resId));\n        } else {\n            resIds = await this.getResIds(true);\n            records = this.records.filter((r) => resIds.includes(r.resId));\n        }\n        const unlinked = await this.model.orm.unlink(this.resModel, resIds, {\n            context: this.context,\n        });\n        if (!unlinked) {\n            return false;\n        }\n        if (\n            this.isDomainSelected &&\n            resIds.length === this.model.activeIdsLimit &&\n            resIds.length < this.count\n        ) {\n            const msg = _t(\n                \"Only the first %(count)s records have been deleted (out of %(total)s selected)\",\n                { count: resIds.length, total: this.count }\n            );\n            this.model.notification.add(msg, { title: _t(\"Warning\") });\n        }\n        await this.model.load();\n        return unlinked;\n    }\n\n    async _leaveSampleMode() {\n        if (this.model.useSampleModel) {\n            await this._load(this.offset, this.limit, this.orderBy, this.domain);\n            this.model.useSampleModel = false;\n        }\n    }\n\n    async _multiSave(record) {\n        const changes = record._getChanges();\n        if (!Object.keys(changes).length || record === this._recordToDiscard) {\n            return;\n        }\n        const validSelection = this.selection.filter((record) => {\n            return Object.keys(changes).every((fieldName) => {\n                if (record._isReadonly(fieldName)) {\n                    return false;\n                } else if (record._isRequired(fieldName) && !changes[fieldName]) {\n                    return false;\n                }\n                return true;\n            });\n        });\n        const canProceed = await this.model.hooks.onWillSaveMulti(record, changes, validSelection);\n        if (canProceed === false) {\n            return false;\n        }\n        if (validSelection.length === 0) {\n            this.model.dialog.add(AlertDialog, {\n                body: _t(\"No valid record to save\"),\n                confirm: () => this.leaveEditMode({ discard: true }),\n                dismiss: () => this.leaveEditMode({ discard: true }),\n            });\n            return false;\n        } else {\n            const resIds = unique(validSelection.map((r) => r.resId));\n            const context = this.context;\n            try {\n                await this.model.orm.write(this.resModel, resIds, changes, { context });\n            } catch (e) {\n                record._discard();\n                this.model._updateConfig(record.config, { mode: \"readonly\" }, { reload: false });\n                throw e;\n            }\n            const records = await this.model._loadRecords({ ...this.config, resIds });\n            for (const record of validSelection) {\n                const serverValues = records.find((r) => r.id === record.resId);\n                record._applyValues(serverValues);\n                this.model._updateSimilarRecords(record, serverValues);\n            }\n            record._discard();\n            this.model._updateConfig(record.config, { mode: \"readonly\" }, { reload: false });\n        }\n        this.model.hooks.onSavedMulti(validSelection);\n        return true;\n    }\n\n    async _resequence(originalList, resModel, movedId, targetId) {\n        if (this.resModel === resModel && !this.canResequence()) {\n            return;\n        }\n        const handleField = this.resModel === resModel ? this.handleField : DEFAULT_HANDLE_FIELD;\n        const order = this.orderBy.find((o) => o.name === handleField);\n        const getSequence = (dp) => dp && this._getDPFieldValue(dp, handleField);\n        const getResId = (dp) => this._getDPresId(dp);\n        const resequencedRecords = await resequence({\n            records: originalList,\n            resModel,\n            movedId,\n            targetId,\n            fieldName: handleField,\n            asc: order?.asc,\n            context: this.context,\n            orm: this.model.orm,\n            getSequence,\n            getResId,\n        });\n        if (resequencedRecords) {\n            for (const dpData of resequencedRecords) {\n                const dp = originalList.find((d) => getResId(d) === dpData.id);\n                if (dp instanceof Record) {\n                    dp._applyValues(dpData);\n                } else {\n                    dp[handleField] = dpData[handleField];\n                }\n            }\n        }\n    }\n\n    _selectDomain(value) {\n        this.isDomainSelected = value;\n    }\n\n    async _toggleArchive(isSelected, state) {\n        const method = state ? \"action_archive\" : \"action_unarchive\";\n        const context = this.context;\n        const resIds = await this.getResIds(isSelected);\n        const action = await this.model.orm.call(this.resModel, method, [resIds], { context });\n        if (\n            this.isDomainSelected &&\n            resIds.length === this.model.activeIdsLimit &&\n            resIds.length < this.count\n        ) {\n            const msg = _t(\n                \"Of the %(selectedRecord)s selected records, only the first %(firstRecords)s have been archived/unarchived.\",\n                {\n                    selectedRecords: resIds.length,\n                    firstRecords: this.count,\n                }\n            );\n            this.model.notification.add(msg, { title: _t(\"Warning\") });\n        }\n        const reload = () => this.model.load();\n        if (action && Object.keys(action).length) {\n            this.model.action.doAction(action, {\n                onClose: reload,\n            });\n        } else {\n            return reload();\n        }\n    }\n\n    async _toggleSelection() {\n        if (this.selection.length === this.records.length) {\n            this.records.forEach((record) => {\n                record._toggleSelection(false);\n            });\n            this._selectDomain(false);\n        } else {\n            this.records.forEach((record) => {\n                record._toggleSelection(true);\n            });\n        }\n    }\n}\n", "import { DynamicList } from \"./dynamic_list\";\n\nexport class DynamicRecordList extends DynamicList {\n    static type = \"DynamicRecordList\";\n\n    /**\n     * @param {import(\"./relational_model\").Config} config\n     * @param {Object} data\n     */\n    setup(config, data) {\n        super.setup(config);\n        this._setData(data);\n    }\n\n    _setData(data) {\n        /** @type {import(\"./record\").Record[]} */\n        this.records = data.records.map((r) => this._createRecordDatapoint(r));\n        this._updateCount(data);\n        this._selectDomain(this.isDomainSelected);\n    }\n\n    // -------------------------------------------------------------------------\n    // Getter\n    // -------------------------------------------------------------------------\n\n    get hasData() {\n        return this.count > 0;\n    }\n\n    // -------------------------------------------------------------------------\n    // Public\n    // -------------------------------------------------------------------------\n\n    /**\n     * @param {number} resId\n     * @param {boolean} [atFirstPosition]\n     * @returns {Promise<Record>} the newly created record\n     */\n    addExistingRecord(resId, atFirstPosition) {\n        return this.model.mutex.exec(async () => {\n            const record = this._createRecordDatapoint({});\n            await record._load({ resId });\n            this._addRecord(record, atFirstPosition ? 0 : this.records.length);\n            return record;\n        });\n    }\n\n    /**\n     * @param {boolean} [atFirstPosition=false]\n     * @returns {Promise<Record>}\n     */\n    addNewRecord(atFirstPosition = false) {\n        return this.model.mutex.exec(async () => {\n            await this._leaveSampleMode();\n            return this._addNewRecord(atFirstPosition);\n        });\n    }\n\n    /**\n     * Performs a search_count with the current domain to set the count. This is\n     * useful as web_search_read limits the count for performance reasons, so it\n     * might sometimes be less than the real number of records matching the domain.\n     **/\n    async fetchCount() {\n        this.count = await this.model._updateCount(this.config);\n        this.hasLimitedCount = false;\n        return this.count;\n    }\n\n    moveRecord(dataRecordId, _dataGroupId, refId, _targetGroupId) {\n        return this.resequence(dataRecordId, refId);\n    }\n\n    removeRecord(record) {\n        if (!record.isNew) {\n            throw new Error(\"removeRecord can't be called on an existing record\");\n        }\n        const index = this.records.findIndex((r) => r === record);\n        if (index < 0) {\n            return;\n        }\n        this.records.splice(index, 1);\n        this.count--;\n        return record;\n    }\n\n    async resequence(movedRecordId, targetRecordId) {\n        return this.model.mutex.exec(\n            async () =>\n                await this._resequence(this.records, this.resModel, movedRecordId, targetRecordId)\n        );\n    }\n\n    // -------------------------------------------------------------------------\n    // Protected\n    // -------------------------------------------------------------------------\n\n    async _addNewRecord(atFirstPosition) {\n        const values = await this.model._loadNewRecord({\n            resModel: this.resModel,\n            activeFields: this.activeFields,\n            fields: this.fields,\n            context: this.context,\n        });\n        const record = this._createRecordDatapoint(values, \"edit\");\n        this._addRecord(record, atFirstPosition ? 0 : this.records.length);\n        return record;\n    }\n\n    _addRecord(record, index) {\n        this.records.splice(Number.isInteger(index) ? index : this.records.length, 0, record);\n        this.count++;\n    }\n\n    _createRecordDatapoint(data, mode = \"readonly\") {\n        return new this.model.constructor.Record(\n            this.model,\n            {\n                context: this.context,\n                activeFields: this.activeFields,\n                resModel: this.resModel,\n                fields: this.fields,\n                resId: data.id || false,\n                resIds: data.id ? [data.id] : [],\n                isMonoRecord: true,\n                currentCompanyId: this.currentCompanyId,\n                mode,\n            },\n            data,\n            { manuallyAdded: !data.id }\n        );\n    }\n\n    _getDPresId(record) {\n        return record.resId;\n    }\n\n    _getDPFieldValue(record, handleField) {\n        return record.data[handleField];\n    }\n\n    async _load(offset, limit, orderBy, domain) {\n        await this.model._updateConfig(\n            this.config,\n            { offset, limit, orderBy, domain },\n            { commit: this._setData.bind(this) }\n        );\n    }\n\n    _removeRecords(recordIds) {\n        const keptRecords = this.records.filter((r) => !recordIds.includes(r.id));\n        this.count -= this.records.length - keptRecords.length;\n        this.records = keptRecords;\n        if (this.offset && !this.records.length) {\n            // we weren't on the first page, and we removed all records of the current page\n            const offset = Math.max(this.offset - this.limit, 0);\n            this.model._updateConfig(this.config, { offset }, { reload: false });\n        }\n    }\n\n    _selectDomain(value) {\n        if (value) {\n            this.records.forEach((r) => (r.selected = true));\n        }\n        super._selectDomain(value);\n    }\n\n    _updateCount(data) {\n        const length = data.length;\n        if (length >= this.config.countLimit + 1) {\n            this.hasLimitedCount = true;\n            this.count = this.config.countLimit;\n        } else {\n            this.hasLimitedCount = false;\n            this.count = length;\n        }\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class FetchRecordError extends Error {\n    constructor(resIds) {\n        super(\n            _t(\n                \"It seems the records with IDs %s cannot be found. They might have been deleted.\",\n                resIds\n            )\n        );\n        this.resIds = resIds;\n    }\n}\nfunction fetchRecordErrorHandler(env, error, originalError) {\n    if (originalError instanceof FetchRecordError) {\n        env.services.notification.add(originalError.message, { sticky: true, type: \"danger\" });\n        return true;\n    }\n}\nconst errorHandlerRegistry = registry.category(\"error_handlers\");\nerrorHandlerRegistry.add(\"fetchRecordErrorHandler\", fetchRecordErrorHandler);\n", "import { Domain } from \"@web/core/domain\";\nimport { DataPoint } from \"./datapoint\";\n\n/**\n * @typedef Params\n * @property {string[]} groupBy\n */\n\nexport class Group extends DataPoint {\n    static type = \"Group\";\n\n    /**\n     * @param {import(\"./relational_model\").Config} config\n     */\n    setup(config, data) {\n        super.setup(...arguments);\n        this.groupByField = this.fields[config.groupByFieldName];\n        this.range = data.range;\n        this._rawValue = data.rawValue;\n        /** @type {number} */\n        this.count = data.count;\n        this.value = data.value;\n        this.serverValue = data.serverValue;\n        this.displayName = data.displayName;\n        this.aggregates = data.aggregates;\n        let List;\n        if (config.list.groupBy.length) {\n            List = this.model.constructor.DynamicGroupList;\n        } else {\n            List = this.model.constructor.DynamicRecordList;\n        }\n        /** @type {import(\"./dynamic_group_list\").DynamicGroupList | import(\"./dynamic_record_list\").DynamicRecordList} */\n        this.list = new List(this.model, config.list, data);\n        this._useGroupCountForList();\n        if (config.record) {\n            config.record.context = { ...config.record.context, ...config.context };\n            this.record = new this.model.constructor.Record(this.model, config.record, data.values);\n        }\n    }\n\n    // -------------------------------------------------------------------------\n    // Getters\n    // -------------------------------------------------------------------------\n\n    get groupDomain() {\n        return this.config.initialDomain;\n    }\n    get hasData() {\n        return this.count > 0;\n    }\n    get isFolded() {\n        return this.config.isFolded;\n    }\n    get records() {\n        return this.list.records;\n    }\n\n    // -------------------------------------------------------------------------\n    // Public\n    // -------------------------------------------------------------------------\n\n    async addExistingRecord(resId, atFirstPosition = false) {\n        const record = await this.list.addExistingRecord(resId, atFirstPosition);\n        this.count++;\n        return record;\n    }\n\n    async addNewRecord(_unused, atFirstPosition = false) {\n        const canProceed = await this.model.root.leaveEditMode();\n        if (canProceed) {\n            const record = await this.list.addNewRecord(atFirstPosition);\n            if (record) {\n                this.count++;\n            }\n        }\n    }\n\n    async applyFilter(filter) {\n        if (filter) {\n            await this.list.load({\n                domain: Domain.and([this.groupDomain, filter]).toList(),\n            });\n        } else {\n            await this.list.load({ domain: this.groupDomain });\n            this.count = this.list.isGrouped ? this.list.recordCount : this.list.count;\n        }\n        this.model._updateConfig(this.config, { extraDomain: filter }, { reload: false });\n    }\n\n    deleteRecords(records) {\n        return this.model.mutex.exec(() => this._deleteRecords(records));\n    }\n\n    async toggle() {\n        if (this.config.isFolded) {\n            await this.list.load();\n        }\n        this._useGroupCountForList();\n        this.model._updateConfig(\n            this.config,\n            { isFolded: !this.config.isFolded },\n            { reload: false }\n        );\n    }\n\n    // -------------------------------------------------------------------------\n    // Protected\n    // -------------------------------------------------------------------------\n\n    _addRecord(record, index) {\n        this.list._addRecord(record, index);\n        this.count++;\n    }\n\n    async _deleteRecords(records) {\n        await this.list._deleteRecords(records);\n        this.count -= records.length;\n    }\n\n    /**\n     * The count returned by web_search_read is limited (see DEFAULT_COUNT_LIMIT). However, the one\n     * returned by web_read_group, for each group, isn't. So in the grouped case, it might happen\n     * that the group count is more accurate than the list one. It that case, we use it on the list.\n     */\n    _useGroupCountForList() {\n        if (!this.list.isGrouped && this.list.count === this.list.config.countLimit) {\n            this.list.count = this.count;\n        }\n    }\n\n    async _removeRecords(recordIds) {\n        const idsToRemove = recordIds.filter((id) => this.list.records.some((r) => r.id === id));\n        this.list._removeRecords(idsToRemove);\n        this.count -= idsToRemove.length;\n    }\n}\n", "import { markRaw, markup, toRaw } from \"@odoo/owl\";\nimport { AlertDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { serializeDate, serializeDateTime } from \"@web/core/l10n/dates\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { x2ManyCommands } from \"@web/core/orm_service\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { escape } from \"@web/core/utils/strings\";\nimport { DataPoint } from \"./datapoint\";\nimport {\n    createPropertyActiveField,\n    getBasicEvalContext,\n    getFieldContext,\n    getFieldsSpec,\n    parseServerValue,\n} from \"./utils\";\nimport { FetchRecordError } from \"./errors\";\n\nexport class Record extends DataPoint {\n    static type = \"Record\";\n\n    /**\n     * @param {import(\"./relational_model\").Config} config\n     * @param {Object} data\n     * @param {Object} [options={}]\n     * @param {boolean} [options.manuallyAdded]\n     * @param {Function} [options.onUpdate]\n     * @param {Record} [options.parentRecord]\n     * @param {string} [options.virtualId]\n     */\n    setup(config, data, options = {}) {\n        this._manuallyAdded = options.manuallyAdded === true;\n        this._onUpdate = options.onUpdate || (() => {});\n        this._parentRecord = options.parentRecord;\n        this.canSaveOnUpdate = !options.parentRecord;\n        this._virtualId = options.virtualId || false;\n        this._isEvalContextReady = false;\n\n        // Be careful that pending changes might not have been notified yet, so the \"dirty\" flag may\n        // be false even though there are changes in a field. Consider calling \"isDirty()\" instead.\n        this.dirty = false;\n        this.selected = false;\n\n        this._invalidFields = new Set();\n        this._unsetRequiredFields = markRaw(new Set());\n        this._closeInvalidFieldsNotification = () => {};\n\n        const parentRecord = this._parentRecord;\n        if (parentRecord) {\n            this.evalContext = {\n                get parent() {\n                    return parentRecord.evalContext;\n                },\n            };\n            this.evalContextWithVirtualIds = {\n                get parent() {\n                    return parentRecord.evalContextWithVirtualIds;\n                },\n            };\n        } else {\n            this.evalContext = {};\n            this.evalContextWithVirtualIds = {};\n        }\n        const missingFields = this.fieldNames.filter((fieldName) => !(fieldName in data));\n        data = { ...this._getDefaultValues(missingFields), ...data };\n        // In db, char, text and html fields can be not set (NULL) and set to the empty string. In\n        // the UI, there's no difference, but in the eval context, it's not the same. The next\n        // structure keeps track of the server values we received for those fields (which can thus\n        // be false or a string). This allows us to properly build the eval context, and to always\n        // expose string values (false fallbacks on the empty string) in this.data.\n        this._textValues = markRaw({});\n        this._setData(data);\n    }\n\n    _setData(data) {\n        this._isEvalContextReady = false;\n        if (this.resId) {\n            this._values = this._parseServerValues(data);\n            this._changes = markRaw({});\n            Object.assign(this._textValues, this._getTextValues(data));\n        } else {\n            this._values = markRaw({});\n            const allVals = { ...this._getDefaultValues(), ...data };\n            this._initialChanges = markRaw(this._parseServerValues(allVals));\n            this._changes = markRaw({ ...this._initialChanges });\n            Object.assign(this._textValues, this._getTextValues(allVals));\n        }\n        this.dirty = false;\n        this.data = { ...this._values, ...this._changes };\n        this._setEvalContext();\n        this._initialTextValues = { ...this._textValues };\n\n        this._invalidFields.clear();\n        this._savePoint = undefined;\n    }\n\n    // -------------------------------------------------------------------------\n    // Getter\n    // -------------------------------------------------------------------------\n\n    get canBeAbandoned() {\n        return this.isNew && !this.dirty && this._manuallyAdded;\n    }\n\n    get hasData() {\n        return true;\n    }\n\n    get isActive() {\n        if (\"active\" in this.activeFields) {\n            return this.data.active;\n        } else if (\"x_active\" in this.activeFields) {\n            return this.data.x_active;\n        }\n        return true;\n    }\n\n    get isInEdition() {\n        if (this.config.mode === \"readonly\") {\n            return false;\n        } else {\n            return this.config.mode === \"edit\" || !this.resId;\n        }\n    }\n\n    get isNew() {\n        return !this.resId;\n    }\n\n    get isValid() {\n        return !this._invalidFields.size;\n    }\n\n    get resId() {\n        return this.config.resId;\n    }\n\n    get resIds() {\n        return this.config.resIds;\n    }\n\n    // -------------------------------------------------------------------------\n    // Public\n    // -------------------------------------------------------------------------\n\n    archive() {\n        return this.model.mutex.exec(() => this._toggleArchive(true));\n    }\n\n    async checkValidity({ displayNotification } = {}) {\n        if (!this._urgentSave) {\n            await this.model._askChanges();\n        }\n        return this._checkValidity({ displayNotification });\n    }\n\n    delete() {\n        return this.model.mutex.exec(async () => {\n            const unlinked = await this.model.orm.unlink(this.resModel, [this.resId], {\n                context: this.context,\n            });\n            if (!unlinked) {\n                return false;\n            }\n            const resIds = this.resIds.slice();\n            const index = resIds.indexOf(this.resId);\n            resIds.splice(index, 1);\n            const resId = resIds[Math.min(index, resIds.length - 1)] || false;\n            if (resId) {\n                await this.model.load({ resId, resIds });\n            } else {\n                this.model._updateConfig(this.config, { resId: false }, { reload: false });\n                this.dirty = false;\n                this._changes = markRaw(this._parseServerValues(this._getDefaultValues()));\n                this._values = markRaw({});\n                this._textValues = markRaw({});\n                this.data = { ...this._changes };\n                this._setEvalContext();\n            }\n        });\n    }\n\n    async discard() {\n        if (this.model._closeUrgentSaveNotification) {\n            this.model._closeUrgentSaveNotification();\n        }\n        await this.model._askChanges();\n        return this.model.mutex.exec(() => this._discard());\n    }\n\n    duplicate() {\n        return this.model.mutex.exec(async () => {\n            const kwargs = { context: this.context };\n            const index = this.resIds.indexOf(this.resId);\n            const [resId] = await this.model.orm.call(\n                this.resModel,\n                \"copy\",\n                [[this.resId]],\n                kwargs\n            );\n            const resIds = this.resIds.slice();\n            resIds.splice(index + 1, 0, resId);\n            await this.model.load({ resId, resIds, mode: \"edit\" });\n        });\n    }\n\n    async isDirty() {\n        await this.model._askChanges();\n        return this.dirty;\n    }\n\n    /**\n     * @param {string} fieldName\n     */\n    isFieldInvalid(fieldName) {\n        return this._invalidFields.has(fieldName);\n    }\n\n    load() {\n        if (arguments.length > 0) {\n            throw new Error(\"Record.load() does not accept arguments\");\n        }\n        return this.model.mutex.exec(() => this._load());\n    }\n\n    async save(options) {\n        await this.model._askChanges();\n        return this.model.mutex.exec(() => this._save(options));\n    }\n\n    async setInvalidField(fieldName) {\n        this.dirty = true;\n        return this._setInvalidField(fieldName);\n    }\n\n    async resetFieldValidity(fieldName) {\n        this.dirty = true;\n        return this._resetFieldValidity(fieldName);\n    }\n\n    switchMode(mode) {\n        return this.model.mutex.exec(() => this._switchMode(mode));\n    }\n\n    toggleSelection(selected) {\n        return this.model.mutex.exec(() => {\n            this._toggleSelection(selected);\n        });\n    }\n\n    unarchive() {\n        return this.model.mutex.exec(() => this._toggleArchive(false));\n    }\n\n    update(changes, { save } = {}) {\n        if (this.model._urgentSave) {\n            return this._update(changes);\n        }\n        return this.model.mutex.exec(async () => {\n            await this._update(changes, { withoutOnchange: save });\n            if (save && this.canSaveOnUpdate) {\n                return this._save();\n            }\n        });\n    }\n\n    async urgentSave() {\n        this.model._urgentSave = true;\n        this.model.bus.trigger(\"WILL_SAVE_URGENTLY\");\n        const succeeded = await this._save({ reload: false });\n        this.model._urgentSave = false;\n        return succeeded;\n    }\n\n    // -------------------------------------------------------------------------\n    // Protected\n    // -------------------------------------------------------------------------\n\n    _addSavePoint() {\n        this._savePoint = markRaw({\n            dirty: this.dirty,\n            textValues: { ...this._textValues },\n            changes: { ...this._changes },\n        });\n        for (const fieldName in this._changes) {\n            if ([\"one2many\", \"many2many\"].includes(this.fields[fieldName].type)) {\n                this._changes[fieldName]._addSavePoint();\n            }\n        }\n    }\n\n    _applyChanges(changes, serverChanges = {}) {\n        // We need to generate the undo function before applying the changes\n        const initialTextValues = { ...this._textValues };\n        const initialChanges = { ...this._changes };\n        const initialData = { ...toRaw(this.data) };\n        const invalidFields = [...toRaw(this._invalidFields)];\n        const undoChanges = () => {\n            for (const fieldName of invalidFields) {\n                this.setInvalidField(fieldName);\n            }\n            Object.assign(this.data, initialData);\n            this._changes = markRaw(initialChanges);\n            Object.assign(this._textValues, initialTextValues);\n            this._setEvalContext();\n        };\n\n        // Apply changes\n        for (const fieldName in changes) {\n            const change = changes[fieldName];\n            this._changes[fieldName] = change;\n            this.data[fieldName] = change;\n            if (this.fields[fieldName].type === \"html\") {\n                this._textValues[fieldName] = change === false ? false : change.toString();\n            } else if ([\"char\", \"text\"].includes(this.fields[fieldName].type)) {\n                this._textValues[fieldName] = change;\n            }\n        }\n\n        // Apply server changes\n        const parsedChanges = this._parseServerValues(serverChanges, this.data);\n        for (const fieldName in parsedChanges) {\n            this._changes[fieldName] = parsedChanges[fieldName];\n            this.data[fieldName] = parsedChanges[fieldName];\n        }\n        Object.assign(this._textValues, this._getTextValues(serverChanges));\n\n        this._setEvalContext();\n\n        // mark changed fields as valid if they were not, and re-evaluate required attributes\n        // for all fields, as some of them might still be unset but become valid with those changes\n        this._removeInvalidFields(Object.keys({ ...changes, ...serverChanges }));\n        this._checkValidity({ removeInvalidOnly: true });\n        return undoChanges;\n    }\n\n    _applyDefaultValues() {\n        const fieldNames = this.fieldNames.filter((fieldName) => {\n            return !(fieldName in this.data);\n        });\n        const defaultValues = this._getDefaultValues(fieldNames);\n        if (this.isNew) {\n            this._applyChanges({}, defaultValues);\n        } else {\n            this._applyValues(defaultValues);\n        }\n    }\n\n    _applyValues(values) {\n        const newValues = this._parseServerValues(values);\n        Object.assign(this._values, newValues);\n        for (const fieldName in newValues) {\n            if (fieldName in this._changes) {\n                if ([\"one2many\", \"many2many\"].includes(this.fields[fieldName].type)) {\n                    this._changes[fieldName] = newValues[fieldName];\n                }\n            }\n        }\n        Object.assign(this.data, this._values, this._changes);\n        const textValues = this._getTextValues(values);\n        Object.assign(this._initialTextValues, textValues);\n        Object.assign(this._textValues, textValues, this._getTextValues(this._changes));\n        this._setEvalContext();\n    }\n\n    _checkValidity({ silent, displayNotification, removeInvalidOnly } = {}) {\n        const unsetRequiredFields = new Set();\n        for (const fieldName in this.activeFields) {\n            const fieldType = this.fields[fieldName].type;\n            if (this._isInvisible(fieldName) || this.fields[fieldName].relatedPropertyField) {\n                continue;\n            }\n            switch (fieldType) {\n                case \"boolean\":\n                case \"float\":\n                case \"integer\":\n                case \"monetary\":\n                    continue;\n                case \"html\":\n                    if (this._isRequired(fieldName) && this.data[fieldName].length === 0) {\n                        unsetRequiredFields.add(fieldName);\n                    }\n                    break;\n                case \"one2many\":\n                case \"many2many\": {\n                    const list = this.data[fieldName];\n                    if (\n                        (this._isRequired(fieldName) && !list.count) ||\n                        !list.records.every(\n                            (r) => !r.dirty || r._checkValidity({ silent, removeInvalidOnly })\n                        )\n                    ) {\n                        unsetRequiredFields.add(fieldName);\n                    }\n                    break;\n                }\n                case \"properties\": {\n                    const value = this.data[fieldName];\n                    if (value) {\n                        const ok = value.every(\n                            (propertyDefinition) =>\n                                propertyDefinition.name &&\n                                propertyDefinition.name.length &&\n                                propertyDefinition.string &&\n                                propertyDefinition.string.length\n                        );\n                        if (!ok) {\n                            unsetRequiredFields.add(fieldName);\n                        }\n                    }\n                    break;\n                }\n                case \"json\": {\n                    if (\n                        this._isRequired(fieldName) &&\n                        (!this.data[fieldName] || !Object.keys(this.data[fieldName]).length)\n                    ) {\n                        unsetRequiredFields.add(fieldName);\n                    }\n                    break;\n                }\n                default:\n                    if (!this.data[fieldName] && this._isRequired(fieldName)) {\n                        unsetRequiredFields.add(fieldName);\n                    }\n            }\n        }\n\n        if (silent) {\n            return !unsetRequiredFields.size;\n        }\n\n        if (removeInvalidOnly) {\n            for (const fieldName of Array.from(this._unsetRequiredFields)) {\n                if (!unsetRequiredFields.has(fieldName)) {\n                    this._unsetRequiredFields.delete(fieldName);\n                    this._invalidFields.delete(fieldName);\n                }\n            }\n        } else {\n            for (const fieldName of Array.from(this._unsetRequiredFields)) {\n                this._invalidFields.delete(fieldName);\n            }\n            this._unsetRequiredFields.clear();\n            for (const fieldName of unsetRequiredFields) {\n                this._unsetRequiredFields.add(fieldName);\n                this._setInvalidField(fieldName);\n            }\n        }\n        const isValid = !this._invalidFields.size;\n        if (!isValid && displayNotification) {\n            const items = [...this._invalidFields].map((fieldName) => {\n                return `<li>${escape(this.fields[fieldName].string || fieldName)}</li>`;\n            }, this);\n            this._closeInvalidFieldsNotification = this.model.notification.add(\n                markup(`<ul>${items.join(\"\")}</ul>`),\n                {\n                    title: _t(\"Invalid fields: \"),\n                    type: \"danger\",\n                }\n            );\n        }\n        return isValid;\n    }\n\n    /**\n     * Given a possibily incomplete value for a many2one field (i.e. a pair [id, display_name] but\n     * with id and/or display_name being undefined), return the complete value as follows:\n     *  - if a display_name is given but no id, perform a name_create to get an id\n     *  - if an id is given but display_name is undefined, call web_read to get the display_name\n     *  - if both id and display_name are given, return the value as is\n     *  - in any other cases, return false\n     *\n     * @param {Array | false} value a (possibly incomplete) pair [id, display_name] or false\n     * @param {string} fieldName\n     * @param {string} resModel\n     * @returns the completed pair [id, display_name] or false\n     */\n    async _completeMany2OneValue(value, fieldName, resModel) {\n        const resId = value[0];\n        const displayName = value[1];\n        if (!resId && !displayName) {\n            return false;\n        }\n        const context = getFieldContext(this, fieldName);\n        if (!resId && displayName !== undefined) {\n            return this.model.orm.call(resModel, \"name_create\", [displayName], { context });\n        }\n        if (resId && displayName === undefined) {\n            const kwargs = {\n                context,\n                specification: { display_name: {} },\n            };\n            const records = await this.model.orm.webRead(resModel, [resId], kwargs);\n            return [resId, records[0].display_name];\n        }\n        return value;\n    }\n\n    _computeDataContext() {\n        const dataContext = {};\n        const x2manyDataContext = {\n            withVirtualIds: {},\n            withoutVirtualIds: {},\n        };\n        const data = toRaw(this.data);\n        for (const fieldName in data) {\n            const value = data[fieldName];\n            const field = this.fields[fieldName];\n            if (field.relatedPropertyField) {\n                continue;\n            }\n            if ([\"char\", \"text\", \"html\"].includes(field.type)) {\n                dataContext[fieldName] = this._textValues[fieldName];\n            } else if (field.type === \"one2many\" || field.type === \"many2many\") {\n                x2manyDataContext.withVirtualIds[fieldName] = value.currentIds;\n                x2manyDataContext.withoutVirtualIds[fieldName] = value.currentIds.filter(\n                    (id) => typeof id === \"number\"\n                );\n            } else if (value && field.type === \"date\") {\n                dataContext[fieldName] = serializeDate(value);\n            } else if (value && field.type === \"datetime\") {\n                dataContext[fieldName] = serializeDateTime(value);\n            } else if (value && field.type === \"many2one\") {\n                dataContext[fieldName] = value[0];\n            } else if (value && field.type === \"reference\") {\n                dataContext[fieldName] = `${value.resModel},${value.resId}`;\n            } else if (field.type === \"properties\") {\n                dataContext[fieldName] = value.filter(\n                    (property) => !property.definition_deleted !== false\n                );\n            } else {\n                dataContext[fieldName] = value;\n            }\n        }\n        dataContext.id = this.resId || false;\n        return {\n            withVirtualIds: { ...dataContext, ...x2manyDataContext.withVirtualIds },\n            withoutVirtualIds: { ...dataContext, ...x2manyDataContext.withoutVirtualIds },\n        };\n    }\n\n    _createStaticListDatapoint(data, fieldName) {\n        const { related, limit, defaultOrderBy } = this.activeFields[fieldName];\n        const config = {\n            resModel: this.fields[fieldName].relation,\n            activeFields: (related && related.activeFields) || {},\n            fields: (related && related.fields) || {},\n            relationField: this.fields[fieldName].relation_field || false,\n            offset: 0,\n            resIds: data.map((r) => r.id),\n            orderBy: defaultOrderBy || [],\n            limit: limit || Number.MAX_SAFE_INTEGER,\n            currentCompanyId: this.currentCompanyId,\n            context: {}, // will be set afterwards, see \"_updateContext\" in \"_setEvalContext\"\n        };\n        const options = {\n            onUpdate: ({ withoutOnchange } = {}) =>\n                this._update({ [fieldName]: [] }, { withoutOnchange }),\n            parent: this,\n        };\n        return new this.model.constructor.StaticList(this.model, config, data, options);\n    }\n\n    _discard() {\n        for (const fieldName in this._changes) {\n            if ([\"one2many\", \"many2many\"].includes(this.fields[fieldName].type)) {\n                this._changes[fieldName]._discard();\n            }\n        }\n        if (this._savePoint) {\n            this.dirty = this._savePoint.dirty;\n            this._changes = markRaw({ ...this._savePoint.changes });\n            this._textValues = markRaw({ ...this._savePoint.textValues });\n        } else {\n            this.dirty = false;\n            this._changes = markRaw(this.isNew ? { ...this._initialChanges } : {});\n            this._textValues = markRaw({ ...this._initialTextValues });\n        }\n        this.data = { ...this._values, ...this._changes };\n        this._savePoint = undefined;\n        this._setEvalContext();\n        this._invalidFields.clear();\n        this._closeInvalidFieldsNotification();\n        this._closeInvalidFieldsNotification = () => {};\n        this._restoreActiveFields();\n    }\n\n    _formatServerValue(fieldType, value) {\n        if (fieldType === \"date\") {\n            return value ? serializeDate(value) : false;\n        } else if (fieldType === \"datetime\") {\n            return value ? serializeDateTime(value) : false;\n        } else if (fieldType === \"char\" || fieldType === \"text\") {\n            return value !== \"\" ? value : false;\n        } else if (fieldType === \"html\") {\n            return value && value.length ? value : false;\n        } else if (fieldType === \"many2one\") {\n            return value ? value[0] : false;\n        } else if (fieldType === \"many2one_reference\") {\n            return value ? value.resId : 0;\n        } else if (fieldType === \"reference\") {\n            return value && value.resModel && value.resId\n                ? `${value.resModel},${value.resId}`\n                : false;\n        } else if (fieldType === \"properties\") {\n            return value.map((property) => {\n                let value;\n                if (property.type === \"many2one\") {\n                    value = property.value;\n                } else if (\n                    (property.type === \"date\" || property.type === \"datetime\") &&\n                    typeof property.value === \"string\"\n                ) {\n                    // TO REMOVE: need refactoring PropertyField to use the same format as the server\n                    value = property.value;\n                } else {\n                    value = this._formatServerValue(property.type, property.value);\n                }\n                return {\n                    ...property,\n                    value,\n                };\n            });\n        }\n        return value;\n    }\n\n    _getChanges(changes = this._changes, { withReadonly } = {}) {\n        const result = {};\n        for (const [fieldName, value] of Object.entries(changes)) {\n            const field = this.fields[fieldName];\n            if (fieldName === \"id\") {\n                continue;\n            }\n            if (\n                !withReadonly &&\n                fieldName in this.activeFields &&\n                this._isReadonly(fieldName) &&\n                !this.activeFields[fieldName].forceSave\n            ) {\n                continue;\n            }\n            if (field.relatedPropertyField) {\n                continue;\n            }\n            if (field.type === \"one2many\" || field.type === \"many2many\") {\n                const commands = value._getCommands({ withReadonly });\n                if (!this.isNew && !commands.length && !withReadonly) {\n                    continue;\n                }\n                result[fieldName] = commands;\n            } else {\n                result[fieldName] = this._formatServerValue(field.type, value);\n            }\n        }\n        return result;\n    }\n\n    _getDefaultValues(fieldNames = this.fieldNames) {\n        const defaultValues = {};\n        for (const fieldName of fieldNames) {\n            switch (this.fields[fieldName].type) {\n                case \"integer\":\n                case \"float\":\n                case \"monetary\":\n                    defaultValues[fieldName] = fieldName === \"id\" ? false : 0;\n                    break;\n                case \"one2many\":\n                case \"many2many\":\n                    defaultValues[fieldName] = [];\n                    break;\n                default:\n                    defaultValues[fieldName] = false;\n            }\n        }\n        return defaultValues;\n    }\n\n    _getTextValues(values) {\n        const textValues = {};\n        for (const fieldName in values) {\n            if (!this.activeFields[fieldName]) {\n                continue;\n            }\n            if ([\"char\", \"text\", \"html\"].includes(this.fields[fieldName].type)) {\n                textValues[fieldName] = values[fieldName];\n            }\n        }\n        return textValues;\n    }\n\n    _isInvisible(fieldName) {\n        const invisible = this.activeFields[fieldName].invisible;\n        return invisible ? evaluateBooleanExpr(invisible, this.evalContextWithVirtualIds) : false;\n    }\n\n    _isReadonly(fieldName) {\n        const readonly = this.activeFields[fieldName].readonly;\n        return readonly ? evaluateBooleanExpr(readonly, this.evalContextWithVirtualIds) : false;\n    }\n\n    _isRequired(fieldName) {\n        const required = this.activeFields[fieldName].required;\n        return required ? evaluateBooleanExpr(required, this.evalContextWithVirtualIds) : false;\n    }\n\n    async _load(nextConfig = {}) {\n        if (\"resId\" in nextConfig && this.resId) {\n            throw new Error(\"Cannot change resId of a record\");\n        }\n        await this.model._updateConfig(this.config, nextConfig, {\n            commit: (values) => {\n                if (this.resId) {\n                    this.model._updateSimilarRecords(this, values);\n                }\n                this._setData(values);\n            },\n        });\n    }\n\n    /**\n     * This function extracts all properties and adds them to fields and activeFields.\n     * @param {Object[]} properties the list of properties to be extracted\n     * @param {string} fieldName name of the field containing the properties\n     * @param {Array} parent Array with ['id, 'display_name'], representing the record to which the definition of properties is linked\n     * @param {Object} currentValues current values of the record\n     * @returns An object containing as key `${fieldName}.${property.name}` and as value the value of the property\n     */\n    _processProperties(properties, fieldName, parent, currentValues = {}) {\n        const data = {};\n\n        const hasCurrentValues = Object.keys(currentValues).length > 0;\n        for (const property of properties) {\n            const propertyFieldName = `${fieldName}.${property.name}`;\n\n            // Add Unknown Property Field and ActiveField\n            if (hasCurrentValues || !this.fields[propertyFieldName]) {\n                this.fields[propertyFieldName] = {\n                    ...property,\n                    name: propertyFieldName,\n                    relatedPropertyField: {\n                        name: fieldName,\n                    },\n                    propertyName: property.name,\n                    relation: property.comodel,\n                };\n            }\n            if (hasCurrentValues || !this.activeFields[propertyFieldName]) {\n                this.activeFields[propertyFieldName] = createPropertyActiveField(property);\n            }\n\n            if (!this.activeFields[propertyFieldName].relatedPropertyField) {\n                this.activeFields[propertyFieldName].relatedPropertyField = {\n                    name: fieldName,\n                    id: parent?.id,\n                    displayName: parent?.display_name,\n                };\n            }\n\n            // Extract property data\n            if (property.type === \"many2many\") {\n                let staticList = currentValues[propertyFieldName];\n                if (!staticList) {\n                    staticList = this._createStaticListDatapoint(\n                        (property.value || []).map((record) => ({\n                            id: record[0],\n                            display_name: record[1],\n                        })),\n                        propertyFieldName\n                    );\n                }\n                data[propertyFieldName] = staticList;\n            } else if (property.type === \"many2one\") {\n                data[propertyFieldName] =\n                    property.value.length && property.value[1] === null\n                        ? [property.value[0], _t(\"No Access\")]\n                        : property.value;\n            } else {\n                data[propertyFieldName] = property.value ?? false;\n            }\n        }\n\n        return data;\n    }\n\n    _parseServerValues(serverValues, currentValues = {}) {\n        const parsedValues = {};\n        if (!serverValues) {\n            return parsedValues;\n        }\n        for (const fieldName in serverValues) {\n            const value = serverValues[fieldName];\n            if (!this.activeFields[fieldName]) {\n                continue;\n            }\n            const field = this.fields[fieldName];\n            if (field.type === \"one2many\" || field.type === \"many2many\") {\n                let staticList = currentValues[fieldName];\n                let valueIsCommandList = true;\n                // value can be a list of records or a list of commands (new record)\n                valueIsCommandList = value.length > 0 && Array.isArray(value[0]);\n                if (!staticList) {\n                    let data = valueIsCommandList ? [] : value;\n                    if (data.length > 0 && typeof data[0] === \"number\") {\n                        data = data.map((resId) => {\n                            return { id: resId };\n                        });\n                    }\n                    staticList = this._createStaticListDatapoint(data, fieldName);\n                    if (valueIsCommandList) {\n                        staticList._applyInitialCommands(value);\n                    }\n                } else if (valueIsCommandList) {\n                    staticList._applyCommands(value);\n                }\n                parsedValues[fieldName] = staticList;\n            } else {\n                parsedValues[fieldName] = parseServerValue(field, value);\n                if (field.type === \"properties\") {\n                    const parent = serverValues[field.definition_record];\n                    Object.assign(\n                        parsedValues,\n                        this._processProperties(\n                            parsedValues[fieldName],\n                            fieldName,\n                            parent,\n                            currentValues\n                        )\n                    );\n                }\n            }\n        }\n        return parsedValues;\n    }\n\n    async _preprocessMany2oneChanges(changes) {\n        const proms = Object.entries(changes)\n            .filter(([fieldName]) => this.fields[fieldName].type === \"many2one\")\n            .map(async ([fieldName, value]) => {\n                if (!value) {\n                    changes[fieldName] = false;\n                } else if (!this.activeFields[fieldName]) {\n                    changes[fieldName] = value;\n                } else {\n                    const relation = this.fields[fieldName].relation;\n                    return this._completeMany2OneValue(value, fieldName, relation).then((v) => {\n                        changes[fieldName] = v;\n                    });\n                }\n            });\n        return Promise.all(proms);\n    }\n\n    async _preprocessMany2OneReferenceChanges(changes) {\n        const proms = Object.entries(changes)\n            .filter(([fieldName]) => this.fields[fieldName].type === \"many2one_reference\")\n            .map(async ([fieldName, value]) => {\n                if (!value) {\n                    changes[fieldName] = false;\n                } else if (typeof value === \"number\") {\n                    // Many2OneReferenceInteger field only manipulates the id\n                    changes[fieldName] = { resId: value };\n                } else {\n                    const relation = this.data[this.fields[fieldName].model_field];\n                    return this._completeMany2OneValue(\n                        [value.resId, value.displayName],\n                        fieldName,\n                        relation\n                    ).then((v) => {\n                        changes[fieldName] = { resId: v[0], displayName: v[1] };\n                    });\n                }\n            });\n        return Promise.all(proms);\n    }\n\n    async _preprocessReferenceChanges(changes) {\n        const proms = Object.entries(changes)\n            .filter(([fieldName]) => this.fields[fieldName].type === \"reference\")\n            .map(async ([fieldName, value]) => {\n                if (!value) {\n                    changes[fieldName] = false;\n                } else {\n                    return this._completeMany2OneValue(\n                        [value.resId, value.displayName],\n                        fieldName,\n                        value.resModel\n                    ).then((v) => {\n                        changes[fieldName] = {\n                            resId: v[0],\n                            resModel: value.resModel,\n                            displayName: v[1],\n                        };\n                    });\n                }\n            });\n        return Promise.all(proms);\n    }\n\n    async _preprocessX2manyChanges(changes) {\n        for (const [fieldName, value] of Object.entries(changes)) {\n            if (\n                this.fields[fieldName].type !== \"one2many\" &&\n                this.fields[fieldName].type !== \"many2many\"\n            ) {\n                continue;\n            }\n            const list = this.data[fieldName];\n            for (const command of value) {\n                switch (command[0]) {\n                    case x2ManyCommands.SET:\n                        await list._replaceWith(command[2]);\n                        break;\n                    default:\n                        await list._applyCommands([command]);\n                }\n            }\n            changes[fieldName] = list;\n        }\n    }\n\n    _preprocessPropertiesChanges(changes) {\n        for (const [fieldName, value] of Object.entries(changes)) {\n            const field = this.fields[fieldName];\n            if (field.type === \"properties\") {\n                const parent =\n                    changes[field.definition_record] || this.data[field.definition_record];\n                Object.assign(\n                    changes,\n                    this._processProperties(value, fieldName, parent, this.data)\n                );\n            } else if (field && field.relatedPropertyField) {\n                const [propertyFieldName, propertyName] = field.name.split(\".\");\n                const propertiesData = this.data[propertyFieldName] || [];\n                if (!propertiesData.find((property) => property.name === propertyName)) {\n                    // try to change the value of a properties that has a different parent\n                    this.model.notification.add(\n                        _t(\n                            \"This record belongs to a different parent so you can not change this property.\"\n                        ),\n                        { type: \"warning\" }\n                    );\n                    return;\n                }\n                changes[propertyFieldName] = propertiesData.map((property) =>\n                    property.name === propertyName ? { ...property, value } : property\n                );\n            }\n        }\n    }\n\n    _preprocessHtmlChanges(changes) {\n        for (const [fieldName, value] of Object.entries(changes)) {\n            if (this.fields[fieldName].type === \"html\") {\n                changes[fieldName] = value === false ? false : markup(value);\n            }\n        }\n    }\n\n    _removeInvalidFields(fieldNames) {\n        for (const fieldName of fieldNames) {\n            this._invalidFields.delete(fieldName);\n        }\n    }\n\n    _restoreActiveFields() {\n        if (!this._activeFieldsToRestore) {\n            return;\n        }\n        this.model._updateConfig(\n            this.config,\n            {\n                activeFields: { ...this._activeFieldsToRestore },\n            },\n            { reload: false }\n        );\n        this._activeFieldsToRestore = undefined;\n    }\n\n    async _save({ reload = true, onError, nextId } = {}) {\n        if (this.model._closeUrgentSaveNotification) {\n            this.model._closeUrgentSaveNotification();\n        }\n        const creation = !this.resId;\n        if (nextId) {\n            if (creation) {\n                throw new Error(\"Cannot set nextId on a new record\");\n            }\n            reload = true;\n        }\n        // before saving, abandon new invalid, untouched records in x2manys\n        for (const fieldName in this.activeFields) {\n            const field = this.fields[fieldName];\n            if ([\"one2many\", \"many2many\"].includes(field.type) && !field.relatedPropertyField) {\n                this.data[fieldName]._abandonRecords();\n            }\n        }\n        if (!this._checkValidity({ displayNotification: true })) {\n            return false;\n        }\n        const changes = this._getChanges();\n        delete changes.id; // id never changes, and should not be written\n        if (!creation && !Object.keys(changes).length) {\n            if (nextId) {\n                return this.model.load({ resId: nextId });\n            }\n            this._changes = markRaw({});\n            this.data = { ...this._values };\n            this.dirty = false;\n            return true;\n        }\n        if (\n            this.model._urgentSave &&\n            this.model.useSendBeaconToSaveUrgently &&\n            !this.model.env.inDialog\n        ) {\n            // We are trying to save urgently because the user is closing the page. To\n            // ensure that the save succeeds, we can't do a classic rpc, as these requests\n            // can be cancelled (payload too heavy, network too slow, computer too fast...).\n            // We instead use sendBeacon, which isn't cancellable. However, it has limited\n            // payload (typically < 64k). So we try to save with sendBeacon, and if it\n            // doesn't work, we will prevent the page from unloading.\n            const route = `/web/dataset/call_kw/${this.resModel}/web_save`;\n            const params = {\n                model: this.resModel,\n                method: \"web_save\",\n                args: [this.resId ? [this.resId] : [], changes],\n                kwargs: { context: this.context, specification: {} },\n            };\n            const data = { jsonrpc: \"2.0\", method: \"call\", params };\n            const blob = new Blob([JSON.stringify(data)], { type: \"application/json\" });\n            const succeeded = navigator.sendBeacon(route, blob);\n            if (succeeded) {\n                this._changes = markRaw({});\n                this.dirty = false;\n            } else {\n                this.model._closeUrgentSaveNotification = this.model.notification.add(\n                    markup(\n                        _t(\n                            `Heads up! Your recent changes are too large to save automatically. Please click the <i class=\"fa fa-cloud-upload fa-fw\"></i> button now to ensure your work is saved before you exit this tab.`\n                        )\n                    ),\n                    { sticky: true }\n                );\n            }\n            return succeeded;\n        }\n        const canProceed = await this.model.hooks.onWillSaveRecord(this, changes);\n        if (canProceed === false) {\n            return false;\n        }\n        let fieldSpec = {};\n        if (reload) {\n            fieldSpec = getFieldsSpec(\n                this.activeFields,\n                this.fields,\n                getBasicEvalContext(this.config)\n            );\n        }\n        const kwargs = {\n            context: this.context,\n            specification: fieldSpec,\n            next_id: nextId,\n        };\n        let records = [];\n        try {\n            records = await this.model.orm.webSave(\n                this.resModel,\n                this.resId ? [this.resId] : [],\n                changes,\n                kwargs\n            );\n        } catch (e) {\n            if (onError) {\n                return onError(e, { discard: () => this._discard() });\n            }\n            if (!this.isInEdition) {\n                await this._load({});\n            }\n            throw e;\n        }\n        if (reload && !records.length) {\n            throw new FetchRecordError([nextId || this.resId]);\n        }\n        if (creation) {\n            const resId = records[0].id;\n            const resIds = this.resIds.concat([resId]);\n            this.model._updateConfig(this.config, { resId, resIds }, { reload: false });\n        }\n        await this.model.hooks.onRecordSaved(this, changes);\n        if (reload) {\n            if (this.resId) {\n                this.model._updateSimilarRecords(this, records[0]);\n            }\n            if (nextId) {\n                this.model._updateConfig(this.config, { resId: nextId }, { reload: false });\n            }\n            if (this.config.isRoot) {\n                this.model.hooks.onWillLoadRoot(this.config);\n            }\n            this._setData(records[0]);\n        } else {\n            this._values = markRaw({ ...this._values, ...this._changes });\n            if (\"id\" in this.activeFields) {\n                this._values.id = records[0].id;\n            }\n            for (const fieldName in this.activeFields) {\n                const field = this.fields[fieldName];\n                if ([\"one2many\", \"many2many\"].includes(field.type) && !field.relatedPropertyField) {\n                    this._changes[fieldName]?._clearCommands();\n                }\n            }\n            this._changes = markRaw({});\n            this.data = { ...this._values };\n            this.dirty = false;\n        }\n        return true;\n    }\n\n    /**\n     * For owl reactivity, it's better to only update the keys inside the evalContext\n     * instead of replacing the evalContext itself, because a lot of components are\n     * registered to the evalContext (but not necessarily keys inside it), and would\n     * be uselessly re-rendered if we replace it by a brand new object.\n     */\n    _setEvalContext() {\n        const evalContext = getBasicEvalContext(this.config);\n        const dataContext = this._computeDataContext();\n        Object.assign(this.evalContext, evalContext, dataContext.withoutVirtualIds);\n        Object.assign(this.evalContextWithVirtualIds, evalContext, dataContext.withVirtualIds);\n        this._isEvalContextReady = true;\n\n        if (!this._parentRecord || this._parentRecord._isEvalContextReady) {\n            for (const [fieldName, value] of Object.entries(toRaw(this.data))) {\n                if ([\"one2many\", \"many2many\"].includes(this.fields[fieldName].type)) {\n                    value._updateContext(getFieldContext(this, fieldName));\n                }\n            }\n        }\n    }\n\n    async _setInvalidField(fieldName) {\n        const canProceed = this.model.hooks.onWillSetInvalidField(this, fieldName);\n        if (canProceed === false) {\n            return;\n        }\n        if (\n            this.selected &&\n            this.model.multiEdit &&\n            this.model.root._recordToDiscard !== this &&\n            !this._invalidFields.has(fieldName)\n        ) {\n            await this.model.dialog.add(AlertDialog, {\n                body: _t(\"No valid record to save\"),\n                confirm: async () => {\n                    await this.discard();\n                    this.switchMode(\"readonly\");\n                },\n            });\n        }\n        this._invalidFields.add(fieldName);\n    }\n\n    _resetFieldValidity(fieldName) {\n        this._invalidFields.delete(fieldName);\n    }\n\n    _switchMode(mode) {\n        this.model._updateConfig(this.config, { mode }, { reload: false });\n        if (mode === \"readonly\") {\n            this._noUpdateParent = false;\n            this._invalidFields.clear();\n        }\n    }\n\n    /**\n     * @param {boolean} state archive the records if true, otherwise unarchive them\n     */\n    async _toggleArchive(state) {\n        const method = state ? \"action_archive\" : \"action_unarchive\";\n        const action = await this.model.orm.call(this.resModel, method, [[this.resId]], {\n            context: this.context,\n        });\n        if (action && Object.keys(action).length) {\n            this.model.action.doAction(action, { onClose: () => this._load() });\n        } else {\n            return this._load();\n        }\n    }\n\n    _toggleSelection(selected) {\n        if (typeof selected === \"boolean\") {\n            this.selected = selected;\n        } else {\n            this.selected = !this.selected;\n        }\n        if (!this.selected && this.model.root.isDomainSelected) {\n            this.model.root._selectDomain(false);\n        }\n    }\n\n    async _getOnchangeValues(changes) {\n        const onChangeFields = Object.keys(changes).filter(\n            (fieldName) => this.activeFields[fieldName] && this.activeFields[fieldName].onChange\n        );\n        if (!onChangeFields.length) {\n            return {};\n        }\n\n        const localChanges = this._getChanges(\n            { ...this._changes, ...changes },\n            { withReadonly: true }\n        );\n        if (this.config.relationField) {\n            const parentRecord = this._parentRecord;\n            localChanges[this.config.relationField] = parentRecord._getChanges(\n                parentRecord._changes,\n                { withReadonly: true }\n            );\n            if (!this._parentRecord.isNew) {\n                localChanges[this.config.relationField].id = this._parentRecord.resId;\n            }\n        }\n        return this.model._onchange(this.config, {\n            changes: localChanges,\n            fieldNames: onChangeFields,\n            evalContext: toRaw(this.evalContext),\n            onError: (e) => {\n                // We apply changes and revert them after to force a render of the Field components\n                const undoChanges = this._applyChanges(changes);\n                undoChanges();\n                throw e;\n            },\n        });\n    }\n\n    async _update(changes, { withoutOnchange, withoutParentUpdate } = {}) {\n        this.dirty = true;\n        const prom = Promise.all([\n            this._preprocessMany2oneChanges(changes),\n            this._preprocessMany2OneReferenceChanges(changes),\n            this._preprocessReferenceChanges(changes),\n            this._preprocessX2manyChanges(changes),\n            this._preprocessPropertiesChanges(changes),\n            this._preprocessHtmlChanges(changes),\n        ]);\n        if (!this.model._urgentSave) {\n            await prom;\n        }\n        if (this.selected && this.model.multiEdit) {\n            this._applyChanges(changes);\n            return this.model.root._multiSave(this);\n        }\n\n        let onchangeServerValues = {};\n        if (!this.model._urgentSave && !withoutOnchange) {\n            onchangeServerValues = await this._getOnchangeValues(changes);\n        }\n        // changes inside the record set as value for a many2one field must trigger the onchange,\n        // but can't be considered as changes on the parent record, so here we detect if many2one\n        // fields really changed, and if not, we delete them from changes\n        for (const fieldName in changes) {\n            if (this.fields[fieldName].type === \"many2one\") {\n                const curVal = toRaw(this.data[fieldName]);\n                const nextVal = changes[fieldName];\n                if (curVal && nextVal && curVal[0] === nextVal[0] && curVal[1] === nextVal[1]) {\n                    delete changes[fieldName];\n                }\n            }\n        }\n        const undoChanges = this._applyChanges(changes, onchangeServerValues);\n        if (Object.keys(changes).length > 0 || Object.keys(onchangeServerValues).length > 0) {\n            try {\n                await this._onUpdate({ withoutParentUpdate });\n            } catch (e) {\n                undoChanges();\n                throw e;\n            }\n            await this.model.hooks.onRecordChanged(this, this._getChanges());\n        }\n    }\n}\n", "// @ts-check\n\nimport { EventBus, markRaw, toRaw } from \"@odoo/owl\";\nimport { makeContext } from \"@web/core/context\";\nimport { Domain } from \"@web/core/domain\";\nimport { WarningDialog } from \"@web/core/errors/error_dialogs\";\nimport { shallowEqual } from \"@web/core/utils/arrays\";\nimport { KeepLast, Mutex } from \"@web/core/utils/concurrency\";\nimport { orderByToString } from \"@web/search/utils/order_by\";\nimport { Model } from \"../model\";\nimport { DynamicGroupList } from \"./dynamic_group_list\";\nimport { DynamicRecordList } from \"./dynamic_record_list\";\nimport { Group } from \"./group\";\nimport { Record } from \"./record\";\nimport { StaticList } from \"./static_list\";\nimport {\n    extractInfoFromGroupData,\n    getBasicEvalContext,\n    getFieldsSpec,\n    isRelational,\n    makeActiveField,\n} from \"./utils\";\nimport { FetchRecordError } from \"./errors\";\n\n/**\n * @typedef Params\n * @property {Config} config\n * @property {State} [state]\n * @property {Hooks} [hooks]\n * @property {number} [limit]\n * @property {number} [countLimit]\n * @property {number} [groupsLimit]\n * @property {string[]} [defaultOrderBy]\n * @property {string[]} [defaultGroupBy]\n * @property {number} [maxGroupByDepth]\n * @property {boolean} [multiEdit]\n * @property {Object} [groupByInfo]\n * @property {number} [activeIdsLimit]\n * @property {boolean} [useSendBeaconToSaveUrgently]\n */\n\n/**\n * @typedef Config\n * @property {string} resModel\n * @property {Object} fields\n * @property {Object} activeFields\n * @property {object} context\n * @property {boolean} isMonoRecord\n * @property {number} currentCompanyId\n * @property {boolean} isRoot\n * @property {Array} [domain]\n * @property {Array} [groupBy]\n * @property {Array} [orderBy]\n * @property {number} [resId]\n * @property {number[]} [resIds]\n * @property {string} [mode]\n * @property {number} [limit]\n * @property {number} [offset]\n * @property {number} [countLimit]\n * @property {number} [groupsLimit]\n * @property {Object} [groups]\n * @property {Object} [currentGroups] // FIXME: could be cleaned\n * @property {boolean} [openGroupsByDefault]\n */\n\n/**\n * @typedef Hooks\n * @property {(nextConfiguration: Config) => void} [onWillLoadRoot]\n * @property {() => Promise} [onRootLoaded]\n * @property {Function} [onWillSaveRecord]\n * @property {Function} [onRecordSaved]\n * @property {Function} [onWillSaveMulti]\n * @property {Function} [onSavedMulti]\n * @property {Function} [onWillSetInvalidField]\n * @property {Function} [onRecordChanged]\n */\n\n/**\n * @typedef State\n * @property {Config} config\n * @property {Object} specialDataCaches\n */\n\nconst DEFAULT_HOOKS = {\n    onWillLoadRoot: () => {},\n    onRootLoaded: () => {},\n    onWillSaveRecord: () => {},\n    onRecordSaved: () => {},\n    onWillSaveMulti: () => {},\n    onSavedMulti: () => {},\n    onWillSetInvalidField: () => {},\n    onRecordChanged: () => {},\n};\n\nexport class RelationalModel extends Model {\n    static services = [\"action\", \"company\", \"dialog\", \"notification\", \"orm\"];\n    static Record = Record;\n    static Group = Group;\n    static DynamicRecordList = DynamicRecordList;\n    static DynamicGroupList = DynamicGroupList;\n    static StaticList = StaticList;\n    static DEFAULT_LIMIT = 80;\n    static DEFAULT_COUNT_LIMIT = 10000;\n    static DEFAULT_GROUP_LIMIT = 80;\n    static DEFAULT_OPEN_GROUP_LIMIT = 10;\n    static MAX_NUMBER_OPENED_GROUPS = 10;\n\n    /**\n     * @param {Params} params\n     */\n    setup(params, { action, company, dialog, notification }) {\n        this.action = action;\n        this.dialog = dialog;\n        this.notification = notification;\n\n        this.bus = new EventBus();\n\n        this.keepLast = markRaw(new KeepLast());\n        this.mutex = markRaw(new Mutex());\n\n        /** @type {Config} */\n        this.config = {\n            isMonoRecord: false,\n            currentCompanyId: company.currentCompany.id,\n            context: {},\n            ...params.config,\n            isRoot: true,\n        };\n\n        /** @type {Hooks} */\n        this.hooks = Object.assign({}, DEFAULT_HOOKS, params.hooks);\n\n        this.initialLimit = params.limit || this.constructor.DEFAULT_LIMIT;\n        this.initialGroupsLimit = params.groupsLimit;\n        this.initialCountLimit = params.countLimit || this.constructor.DEFAULT_COUNT_LIMIT;\n        this.defaultOrderBy = params.defaultOrderBy;\n        this.defaultGroupBy = params.defaultGroupBy;\n        this.maxGroupByDepth = params.maxGroupByDepth;\n        this.groupByInfo = params.groupByInfo || {};\n        this.multiEdit = params.multiEdit;\n        this.activeIdsLimit = params.activeIdsLimit || Number.MAX_SAFE_INTEGER;\n        this.specialDataCaches = markRaw(params.state?.specialDataCaches || {});\n        this.useSendBeaconToSaveUrgently = params.useSendBeaconToSaveUrgently || false;\n\n        this._urgentSave = false;\n    }\n\n    // -------------------------------------------------------------------------\n    // Public\n    // -------------------------------------------------------------------------\n\n    exportState() {\n        return {\n            config: toRaw(this.config),\n            specialDataCaches: this.specialDataCaches,\n        };\n    }\n\n    hasData() {\n        return this.root.hasData;\n    }\n\n    /**\n     * @param {Object} [params={}]\n     * @param {Comparison | null} [params.comparison]\n     * @param {Context} [params.context]\n     * @param {DomainListRepr} [params.domain]\n     * @param {string[]} [params.groupBy]\n     * @param {Object[]} [params.orderBy]\n     * @returns {Promise<void>}\n     */\n    async load(params = {}) {\n        const config = this._getNextConfig(this.config, params);\n        this.hooks.onWillLoadRoot(config);\n        const data = await this.keepLast.add(this._loadData(config));\n        this.root = this._createRoot(config, data);\n        this.config = config;\n        return this.hooks.onRootLoaded();\n    }\n\n    // -------------------------------------------------------------------------\n    // Protected\n    // -------------------------------------------------------------------------\n\n    /**\n     * If we group by default based on a property, the property might not be loaded in `fields`.\n     */\n    async _getPropertyDefinition(config, propertyFullName) {\n        // dynamically load the property and add the definition in the fields attribute\n        const result = await this.orm.call(\n            config.resModel,\n            \"get_property_definition\",\n            [propertyFullName],\n            { context: config.context }\n        );\n        if (!result) {\n            // the property might have been removed\n            config.groupBy = null;\n        } else {\n            result.propertyName = result.name;\n            result.name = propertyFullName; // \"xxxxx\" -> \"property.xxxxx\"\n            // needed for _applyChanges\n            result.relatedPropertyField = { fieldName: propertyFullName.split(\".\")[0] };\n            result.relation = result.comodel; // match name on field\n            config.fields[propertyFullName] = result;\n        }\n    }\n\n    _askChanges() {\n        const proms = [];\n        this.bus.trigger(\"NEED_LOCAL_CHANGES\", { proms });\n        return Promise.all([...proms, this.mutex.getUnlockedDef()]);\n    }\n\n    /**\n     *\n     * @param {Config} config\n     * @param {*} data\n     * @returns {DataPoint}\n     */\n    _createRoot(config, data) {\n        if (config.isMonoRecord) {\n            return new this.constructor.Record(this, config, data);\n        }\n        if (config.groupBy.length) {\n            return new this.constructor.DynamicGroupList(this, config, data);\n        }\n        return new this.constructor.DynamicRecordList(this, config, data);\n    }\n\n    /**\n     * @param {*} params\n     * @returns {Config}\n     */\n    _getNextConfig(currentConfig, params) {\n        const currentGroupBy = currentConfig.groupBy;\n        const config = Object.assign({}, currentConfig);\n\n        config.context = \"context\" in params ? params.context : config.context;\n        if (currentConfig.isMonoRecord) {\n            config.resId = \"resId\" in params ? params.resId : config.resId;\n            config.resIds = \"resIds\" in params ? params.resIds : config.resIds;\n            if (!config.resIds) {\n                config.resIds = config.resId ? [config.resId] : [];\n            }\n            if (!config.resId && config.mode !== \"edit\") {\n                config.mode = \"edit\";\n            }\n        } else {\n            config.domain = \"domain\" in params ? params.domain : config.domain;\n            config.comparison = \"comparison\" in params ? params.comparison : config.comparison;\n\n            // groupBy\n            config.groupBy = \"groupBy\" in params ? params.groupBy : config.groupBy;\n            // apply default groupBy if any\n            if (this.defaultGroupBy && !config.groupBy.length) {\n                config.groupBy = [this.defaultGroupBy];\n            }\n            // restrict the number of groupbys if requested\n            if (this.maxGroupByDepth) {\n                config.groupBy = config.groupBy.slice(0, this.maxGroupByDepth);\n            }\n\n            // orderBy\n            config.orderBy = \"orderBy\" in params ? params.orderBy : config.orderBy;\n            // re-apply previous orderBy if not given (or no order)\n            if (!config.orderBy.length) {\n                config.orderBy = currentConfig.orderBy || [];\n            }\n            // apply default order if no order\n            if (this.defaultOrderBy && !config.orderBy.length) {\n                config.orderBy = this.defaultOrderBy;\n            }\n\n            // keep current root config if any, if the groupBy parameter is the same\n            if (!shallowEqual(config.groupBy || [], currentGroupBy || [])) {\n                delete config.groups;\n            }\n            if (!config.groupBy.length) {\n                config.orderBy = config.orderBy.filter((order) => order.name !== \"__count\");\n            }\n        }\n        if (!config.isMonoRecord && this.root && params.domain) {\n            // always reset the offset to 0 when reloading from above with a domain\n            const resetOffset = (config) => {\n                config.offset = 0;\n                for (const group of Object.values(config.groups || {})) {\n                    resetOffset(group.list);\n                }\n            };\n            resetOffset(config);\n            if (!!config.groupBy.length !== !!currentGroupBy.length) {\n                // from grouped to ungrouped or the other way around -> force the limit to be reset\n                delete config.limit;\n            }\n        }\n\n        return config;\n    }\n\n    /**\n     *\n     * @param {Config} config\n     */\n    async _loadData(config) {\n        if (config.isMonoRecord) {\n            const evalContext = getBasicEvalContext(config);\n            if (!config.resId) {\n                return this._loadNewRecord(config, { evalContext });\n            }\n            const records = await this._loadRecords(\n                {\n                    ...config,\n                    resIds: [config.resId],\n                },\n                evalContext\n            );\n            return records[0];\n        }\n        if (config.resIds) {\n            // static list\n            const resIds = config.resIds.slice(config.offset, config.offset + config.limit);\n            return this._loadRecords({ ...config, resIds });\n        }\n        if (config.groupBy.length) {\n            return this._loadGroupedList(config);\n        }\n        Object.assign(config, {\n            limit: config.limit || this.initialLimit,\n            countLimit: \"countLimit\" in config ? config.countLimit : this.initialCountLimit,\n            offset: config.offset || 0,\n        });\n        if (config.countLimit !== Number.MAX_SAFE_INTEGER) {\n            config.countLimit = Math.max(config.countLimit, config.offset + config.limit);\n        }\n        const { records, length } = await this._loadUngroupedList({\n            ...config,\n            context: {\n                ...config.context,\n                current_company_id: config.currentCompanyId,\n            },\n        });\n        if (config.offset && !records.length) {\n            config.offset = 0;\n            return this._loadData(config);\n        }\n        return { records, length };\n    }\n\n    /**\n     * @param {Config} config\n     */\n    async _loadGroupedList(config) {\n        config.offset = config.offset || 0;\n        config.limit = config.limit || this.initialGroupsLimit;\n        if (!config.limit) {\n            config.limit = config.openGroupsByDefault\n                ? this.constructor.DEFAULT_OPEN_GROUP_LIMIT\n                : this.constructor.DEFAULT_GROUP_LIMIT;\n        }\n        config.groups = config.groups || {};\n        const firstGroupByName = config.groupBy[0].split(\":\")[0];\n        if (firstGroupByName.includes(\".\")) {\n            if (!config.fields[firstGroupByName]) {\n                await this._getPropertyDefinition(config, firstGroupByName);\n            }\n            const propertiesFieldName = firstGroupByName.split(\".\")[0];\n            if (!config.activeFields[propertiesFieldName]) {\n                // add the properties field so we load its data when reading the records\n                // so when we drag and drop we don't need to fetch the value of the record\n                config.activeFields[propertiesFieldName] = makeActiveField();\n            }\n        }\n        const orderBy = config.orderBy.filter(\n            (o) =>\n                o.name === firstGroupByName ||\n                o.name === \"__count\" ||\n                (o.name in config.activeFields && config.fields[o.name].aggregator !== undefined)\n        );\n        const response = await this._webReadGroup(config, orderBy);\n        const { groups: groupsData, length } = response;\n        const groupBy = config.groupBy.slice(1);\n        const groupByField = config.fields[config.groupBy[0].split(\":\")[0]];\n        const commonConfig = {\n            resModel: config.resModel,\n            fields: config.fields,\n            activeFields: config.activeFields,\n        };\n        let groupRecordConfig;\n        const groupRecordResIds = [];\n        if (this.groupByInfo[firstGroupByName]) {\n            groupRecordConfig = {\n                ...this.groupByInfo[firstGroupByName],\n                resModel: config.fields[firstGroupByName].relation,\n                context: {},\n            };\n        }\n        const proms = [];\n        let nbOpenGroups = 0;\n\n        const groups = [];\n        for (const groupData of groupsData) {\n            const group = extractInfoFromGroupData(groupData, config.groupBy, config.fields);\n            if (!config.groups[group.value]) {\n                config.groups[group.value] = {\n                    ...commonConfig,\n                    groupByFieldName: groupByField.name,\n                    isFolded:\n                        \"__fold\" in groupData ? groupData.__fold : !config.openGroupsByDefault,\n                    extraDomain: false,\n                    value: group.value,\n                    list: {\n                        ...commonConfig,\n                        groupBy,\n                    },\n                };\n                if (isRelational(config.fields[firstGroupByName]) && !group.value) {\n                    // fold the \"unset\" group by default when grouped by many2one\n                    config.groups[group.value].isFolded = true;\n                }\n                if (groupRecordConfig) {\n                    config.groups[group.value].record = {\n                        ...groupRecordConfig,\n                        resId: group.value ?? false,\n                    };\n                }\n            }\n            if (groupRecordConfig) {\n                const resId = config.groups[group.value].record.resId;\n                if (resId) {\n                    groupRecordResIds.push(resId);\n                }\n            }\n            const groupConfig = config.groups[group.value];\n            groupConfig.list.orderBy = config.orderBy;\n            groupConfig.initialDomain = group.domain;\n            if (groupConfig.extraDomain) {\n                groupConfig.list.domain = Domain.and([\n                    group.domain,\n                    groupConfig.extraDomain,\n                ]).toList();\n            } else {\n                groupConfig.list.domain = group.domain;\n            }\n            const context = {\n                ...config.context,\n                [`default_${firstGroupByName}`]: group.serverValue,\n            };\n            groupConfig.list.context = context;\n            groupConfig.context = context;\n            if (groupBy.length) {\n                group.groups = [];\n            } else {\n                group.records = [];\n            }\n            if (!groupConfig.isFolded) {\n                nbOpenGroups++;\n                if (nbOpenGroups > this.constructor.MAX_NUMBER_OPENED_GROUPS) {\n                    groupConfig.isFolded = true;\n                }\n            }\n            if (!groupConfig.isFolded && group.count > 0) {\n                const prom = this._loadData(groupConfig.list).then((response) => {\n                    if (groupBy.length) {\n                        group.groups = response ? response.groups : [];\n                        group.length = response ? response.length : 0;\n                    } else {\n                        group.records = response ? response.records : [];\n                    }\n                });\n                proms.push(prom);\n            }\n            groups.push(group);\n        }\n        if (groupRecordConfig && Object.keys(groupRecordConfig.activeFields).length) {\n            const prom = this._loadRecords({\n                ...groupRecordConfig,\n                resIds: groupRecordResIds,\n            }).then((records) => {\n                for (const group of groups) {\n                    if (!group.value) {\n                        group.values = { id: false };\n                        continue;\n                    }\n                    group.values = records.find((r) => group.value && r.id === group.value);\n                }\n            });\n            proms.push(prom);\n        }\n        await Promise.all(proms);\n\n        // if a group becomes empty at some point (e.g. we dragged its last record out of it), and the view is reloaded\n        // with the same domain and groupbys, we want to keep the empty group in the UI\n        const params = JSON.stringify([\n            config.domain,\n            config.groupBy,\n            config.offset,\n            config.limit,\n            config.orderBy,\n        ]);\n        if (config.currentGroups && config.currentGroups.params === params) {\n            const currentGroups = config.currentGroups.groups;\n            currentGroups.forEach((group, index) => {\n                if (\n                    config.groups[group.value] &&\n                    !groups.some((g) => JSON.stringify(g.value) === JSON.stringify(group.value))\n                ) {\n                    const aggregates = Object.assign({}, group.aggregates);\n                    for (const key in aggregates) {\n                        aggregates[key] = 0;\n                    }\n                    groups.splice(\n                        index,\n                        0,\n                        Object.assign({}, group, { count: 0, length: 0, records: [], aggregates })\n                    );\n                }\n            });\n        }\n        config.currentGroups = { params, groups };\n\n        return { groups, length };\n    }\n\n    /**\n     * @param {Config} config\n     * @param {Object} [params={}]\n     * @returns Promise<Object>\n     */\n    async _loadNewRecord(config, params = {}) {\n        return this._onchange(config, params);\n    }\n\n    /**\n     *\n     * @param {Config} config\n     * @param {object} evalContext\n     * @returns\n     */\n    async _loadRecords(config, evalContext = config.context) {\n        const { resModel, resIds, activeFields, fields, context } = config;\n        if (!resIds.length) {\n            return [];\n        }\n        const fieldSpec = getFieldsSpec(activeFields, fields, evalContext);\n        if (Object.keys(fieldSpec).length > 0) {\n            const kwargs = {\n                context: { bin_size: true, ...context },\n                specification: fieldSpec,\n            };\n            const records = await this.orm.webRead(resModel, resIds, kwargs);\n            if (!records.length) {\n                throw new FetchRecordError(resIds);\n            }\n\n            return records;\n        } else {\n            return resIds.map((resId) => {\n                return { id: resId };\n            });\n        }\n    }\n\n    /**\n     * Load records from the server for an ungrouped list. Return the result\n     * of unity read RPC.\n     *\n     * @param {Config} config\n     * @returns\n     */\n    async _loadUngroupedList(config) {\n        const orderBy = config.orderBy.filter((o) => o.name !== \"__count\");\n        const kwargs = {\n            specification: getFieldsSpec(config.activeFields, config.fields, config.context),\n            offset: config.offset,\n            order: orderByToString(orderBy),\n            limit: config.limit,\n            context: { bin_size: true, ...config.context },\n            count_limit:\n                config.countLimit !== Number.MAX_SAFE_INTEGER ? config.countLimit + 1 : undefined,\n        };\n        return this.orm.webSearchRead(config.resModel, config.domain, kwargs);\n    }\n\n    /**\n     * @param {Config} config\n     * @param {Object} param\n     * @param {Object} [param.changes={}]\n     * @param {string[]} [param.fieldNames=[]]\n     * @param {Object} [param.evalContext=config.context]\n     * @returns Promise<Object>\n     */\n    async _onchange(\n        config,\n        { changes = {}, fieldNames = [], evalContext = config.context, onError }\n    ) {\n        const { fields, activeFields, resModel, resId } = config;\n        let context = config.context;\n        if (fieldNames.length === 1) {\n            const fieldContext = config.activeFields[fieldNames[0]].context;\n            context = makeContext([context, fieldContext], evalContext);\n        }\n        const spec = getFieldsSpec(activeFields, fields, evalContext, { withInvisible: true });\n        const args = [resId ? [resId] : [], changes, fieldNames, spec];\n        let response;\n        try {\n            response = await this.orm.call(resModel, \"onchange\", args, { context });\n        } catch (e) {\n            if (onError) {\n                return onError(e);\n            }\n            throw e;\n        }\n        if (response.warning) {\n            const { type, title, message, className, sticky } = response.warning;\n            if (type === \"dialog\") {\n                this.dialog.add(WarningDialog, { title, message });\n            } else {\n                this.notification.add(message, {\n                    className,\n                    sticky,\n                    title,\n                    type: \"warning\",\n                });\n            }\n        }\n        return response.value;\n    }\n\n    /**\n     *\n     * @param {Config} config\n     * @param {Partial<Config>} patch\n     * @param {Object} [options]\n     * @param {boolean} [options.reload=true]\n     * @param {Function} [options.commit] Function to call once the data has been loaded\n     */\n    async _updateConfig(config, patch, { reload = true, commit } = {}) {\n        const tmpConfig = { ...config, ...patch };\n        markRaw(tmpConfig.activeFields);\n        markRaw(tmpConfig.fields);\n\n        let data;\n        if (reload) {\n            if (tmpConfig.isRoot) {\n                this.hooks.onWillLoadRoot(tmpConfig);\n            }\n            data = await this._loadData(tmpConfig);\n        }\n        Object.assign(config, tmpConfig);\n        if (data && commit) {\n            commit(data);\n        }\n        if (reload && config.isRoot) {\n            return this.hooks.onRootLoaded();\n        }\n    }\n\n    /**\n     *\n     * @param {Config} config\n     * @returns {Promise<number>}\n     */\n    async _updateCount(config) {\n        const count = await this.keepLast.add(\n            this.orm.searchCount(config.resModel, config.domain, { context: config.context })\n        );\n        config.countLimit = Number.MAX_SAFE_INTEGER;\n        return count;\n    }\n\n    /**\n     * When grouped by a many2many field, the same record may be displayed in\n     * several groups. When one of these records is edited, we want all other\n     * occurrences to be updated. The purpose of this function is to find and\n     * update all occurrences of a record that has been reloaded, in a grouped\n     * list view.\n     */\n    _updateSimilarRecords(reloadedRecord, serverValues) {\n        if (this.config.isMonoRecord || !this.config.groupBy.length) {\n            return;\n        }\n        for (const record of this.root.records) {\n            if (record === reloadedRecord) {\n                continue;\n            }\n            if (record.resId === reloadedRecord.resId) {\n                record._applyValues(serverValues);\n            }\n        }\n    }\n\n    async _webReadGroup(config, orderBy) {\n        const aggregates = Object.values(config.fields)\n            .filter(\n                (field) =>\n                    field.aggregator &&\n                    field.name in config.activeFields &&\n                    field.name !== config.groupBy[0]\n            )\n            .map((field) => `${field.name}:${field.aggregator}`);\n        return this.orm.webReadGroup(\n            config.resModel,\n            config.domain,\n            aggregates,\n            [config.groupBy[0]],\n            {\n                orderby: orderByToString(orderBy),\n                lazy: true,\n                offset: config.offset,\n                limit: config.limit, // TODO: remove limit when == MAX_integer\n                context: config.context,\n            }\n        );\n    }\n}\n", "import { x2ManyCommands } from \"@web/core/orm_service\";\nimport { intersection } from \"@web/core/utils/arrays\";\nimport { pick } from \"@web/core/utils/objects\";\nimport { completeActiveFields } from \"@web/model/relational_model/utils\";\nimport { DataPoint } from \"./datapoint\";\nimport { fromUnityToServerValues, getBasicEvalContext, getId, patchActiveFields } from \"./utils\";\n\nimport { markRaw } from \"@odoo/owl\";\n\nfunction compareFieldValues(v1, v2, fieldType) {\n    if (fieldType === \"many2one\") {\n        v1 = v1 ? v1[1] : \"\";\n        v2 = v2 ? v2[1] : \"\";\n    }\n    return v1 < v2;\n}\n\nfunction compareRecords(r1, r2, orderBy, fields) {\n    const { name, asc } = orderBy[0];\n    function getValue(record, fieldName) {\n        return fieldName === \"id\" ? record.resId : record.data[fieldName];\n    }\n    const v1 = asc ? getValue(r1, name) : getValue(r2, name);\n    const v2 = asc ? getValue(r2, name) : getValue(r1, name);\n    if (compareFieldValues(v1, v2, fields[name].type)) {\n        return -1;\n    }\n    if (compareFieldValues(v2, v1, fields[name].type)) {\n        return 1;\n    }\n    if (orderBy.length > 1) {\n        return compareRecords(r1, r2, orderBy.slice(1), fields);\n    }\n    return 0;\n}\n\nexport class StaticList extends DataPoint {\n    static type = \"StaticList\";\n\n    /**\n     * @param {import(\"./relational_model\").Config} config\n     * @param {Object} data\n     * @param {Object} [options={}]\n     * @param {Function} [options.onUpdate]\n     * @param {Record} [options.parent]\n     */\n    setup(config, data, options = {}) {\n        this._parent = options.parent;\n        this._onUpdate = options.onUpdate;\n\n        this._cache = markRaw({});\n        this._commands = [];\n        this._initialCommands = [];\n        this._savePoint = undefined;\n        this._unknownRecordCommands = {}; // tracks update commands on records we haven't fetched yet\n        this._currentIds = [...this.resIds];\n        this._initialCurrentIds = [...this.currentIds];\n        this._needsReordering = false;\n        this._tmpIncreaseLimit = 0;\n        // In kanban and non editable list views, x2many records can be opened in a form view in\n        // dialog, which may contain other fields than the kanban or list view. The next set keeps\n        // tracks of records we already opened in dialog and thus for which we already modified the\n        // config to add the form view's fields in activeFields.\n        this._extendedRecords = new Set();\n\n        this.records = data\n            .slice(this.offset, this.limit)\n            .map((r) => this._createRecordDatapoint(r));\n        this.count = this.resIds.length;\n        this.handleField = Object.keys(this.activeFields).find(\n            (fieldName) => this.activeFields[fieldName].isHandle\n        );\n    }\n\n    // -------------------------------------------------------------------------\n    // Getters\n    // -------------------------------------------------------------------------\n\n    get currentIds() {\n        return this._currentIds;\n    }\n\n    get editedRecord() {\n        return this.records.find((record) => record.isInEdition);\n    }\n\n    get evalContext() {\n        const evalContext = getBasicEvalContext(this.config);\n        evalContext.parent = this._parent.evalContext;\n        return evalContext;\n    }\n\n    get limit() {\n        return this.config.limit;\n    }\n\n    get offset() {\n        return this.config.offset;\n    }\n\n    get orderBy() {\n        return this.config.orderBy;\n    }\n\n    get resIds() {\n        return this.config.resIds;\n    }\n\n    // -------------------------------------------------------------------------\n    // Public\n    // -------------------------------------------------------------------------\n\n    /**\n     * Adds a new record to an x2many relation. If params.record is given, adds\n     * given record (use case: after saving the form dialog in a, e.g., non\n     * editable x2many list). Otherwise, do an onchange to get the initial\n     * values and create a new Record (e.g. after clicking on Add a line in an\n     * editable x2many list).\n     *\n     * @param {Object} params\n     * @param {\"top\"|\"bottom\"} [params.position]\n     * @param {Object} [params.activeFields=this.activeFields]\n     * @param {boolean} [params.withoutParent=false]\n     */\n    addNewRecord(params) {\n        return this.model.mutex.exec(async () => {\n            const { activeFields, context, mode, position, withoutParent } = params;\n            const record = await this._createNewRecordDatapoint({\n                activeFields,\n                context,\n                position,\n                withoutParent,\n                manuallyAdded: true,\n                mode,\n            });\n            await this._addRecord(record, { position });\n            await this._onUpdate({ withoutOnchange: !record._checkValidity({ silent: true }) });\n            return record;\n        });\n    }\n\n    canResequence() {\n        return this.handleField && this.orderBy.length && this.orderBy[0].name === this.handleField;\n    }\n\n    delete(record) {\n        return this.model.mutex.exec(async () => {\n            await this._applyCommands([[x2ManyCommands.DELETE, record.resId || record._virtualId]]);\n            await this._onUpdate();\n        });\n    }\n\n    async enterEditMode(record) {\n        const canProceed = await this.leaveEditMode();\n        if (canProceed) {\n            await record.switchMode(\"edit\");\n        }\n        return canProceed;\n    }\n\n    /**\n     * This method is meant to be used in a very specific usecase: when an x2many record is viewed\n     * or edited through a form view dialog (e.g. x2many kanban or non editable list). In this case,\n     * the form typically contains different fields than the kanban or list, so we need to \"extend\"\n     * the fields and activeFields. If the record opened in a form view dialog already exists, we\n     * modify it's config to add the new fields. If it is a new record, we create it with the\n     * extended config.\n     *\n     * @param {Object} params\n     * @param {Object} params.activeFields\n     * @param {Object} params.fields\n     * @param {Object} [params.context]\n     * @param {boolean} [params.withoutParent]\n     * @param {string} [params.mode]\n     * @param {Record} [record]\n     * @returns {Record}\n     */\n    extendRecord(params, record) {\n        return this.model.mutex.exec(async () => {\n            // extend fields and activeFields of the list with those given in params\n            completeActiveFields(this.config.activeFields, params.activeFields);\n            Object.assign(this.fields, params.fields);\n            const activeFields = { ...params.activeFields };\n            for (const fieldName in this.activeFields) {\n                if (fieldName in activeFields) {\n                    patchActiveFields(activeFields[fieldName], this.activeFields[fieldName]);\n                } else {\n                    activeFields[fieldName] = this.activeFields[fieldName];\n                }\n            }\n\n            if (record) {\n                record._noUpdateParent = true;\n                record._activeFieldsToRestore = { ...this.config.activeFields };\n                const config = {\n                    ...record.config,\n                    ...params,\n                    activeFields,\n                };\n\n                // case 1: the record already exists\n                if (this._extendedRecords.has(record.id)) {\n                    // case 1.1: the record has already been extended\n                    // -> simply store a savepoint\n                    this.model._updateConfig(record.config, config, { reload: false });\n                    record._addSavePoint();\n                    return record;\n                }\n                // case 1.2: the record is extended for the first time, and it now potentially has\n                // more fields than before (or x2many fields displayed differently)\n                // -> if it isn't a new record, load it to retrieve the values of new fields\n                // -> generate default values for new fields\n                // -> recursively update the config of the record and it's sub datapoints\n                // -> apply the loaded values in the case of a not new record\n                // -> store a savepoint\n                // These operations must be done in that specific order to ensure that the model is\n                // mutated only once (in a tick), and that datapoints have the correct config to\n                // handle field values they receive.\n                let data = {};\n                if (!record.isNew) {\n                    const evalContext = Object.assign({}, record.evalContext, config.context);\n                    const resIds = [record.resId];\n                    [data] = await this.model._loadRecords({ ...config, resIds }, evalContext);\n                }\n                this.model._updateConfig(record.config, config, { reload: false });\n                record._applyDefaultValues();\n                for (const fieldName in record.activeFields) {\n                    if ([\"one2many\", \"many2many\"].includes(record.fields[fieldName].type)) {\n                        const list = record.data[fieldName];\n                        const patch = {\n                            activeFields: activeFields[fieldName].related.activeFields,\n                            fields: activeFields[fieldName].related.fields,\n                        };\n                        for (const subRecord of Object.values(list._cache)) {\n                            this.model._updateConfig(subRecord.config, patch, {\n                                reload: false,\n                            });\n                        }\n                        this.model._updateConfig(list.config, patch, { reload: false });\n                    }\n                }\n                record._applyValues(data);\n                const commands = this._unknownRecordCommands[record.resId];\n                delete this._unknownRecordCommands[record.resId];\n                if (commands) {\n                    this._applyCommands(commands);\n                }\n                record._addSavePoint();\n            } else {\n                // case 2: the record is a new record\n                // -> simply create one with the extended config\n                record = await this._createNewRecordDatapoint({\n                    activeFields,\n                    context: params.context,\n                    withoutParent: params.withoutParent,\n                    manuallyAdded: true,\n                });\n                record._activeFieldsToRestore = { ...this.config.activeFields };\n                record._noUpdateParent = true;\n            }\n            // mark the record as being extended, to go through case 1.1 next time\n            this._extendedRecords.add(record.id);\n\n            return record;\n        });\n    }\n\n    forget(record) {\n        return this.model.mutex.exec(async () => {\n            await this._applyCommands([[x2ManyCommands.UNLINK, record.resId]]);\n            await this._onUpdate();\n        });\n    }\n\n    async leaveEditMode({ discard, canAbandon, validate } = {}) {\n        if (this.editedRecord) {\n            await this.model._askChanges(false);\n        }\n        return this.model.mutex.exec(async () => {\n            let editedRecord = this.editedRecord;\n            if (editedRecord) {\n                const isValid = editedRecord._checkValidity();\n                if (!isValid && validate) {\n                    return false;\n                }\n                if (canAbandon !== false && !validate) {\n                    this._abandonRecords([editedRecord], { force: true });\n                }\n                // if we still have an editedRecord, it means it hasn't been abandonned\n                editedRecord = this.editedRecord;\n                if (editedRecord) {\n                    if (isValid && !editedRecord.dirty && discard) {\n                        return false;\n                    }\n                    if (isValid || (!editedRecord.dirty && !editedRecord._manuallyAdded)) {\n                        editedRecord._switchMode(\"readonly\");\n                    }\n                }\n            }\n            return !this.editedRecord;\n        });\n    }\n\n    linkTo(resId, serverData) {\n        return this.model.mutex.exec(async () => {\n            await this._applyCommands([[x2ManyCommands.LINK, resId, serverData]]);\n            await this._onUpdate();\n        });\n    }\n\n    unlinkFrom(resId, serverData) {\n        return this.model.mutex.exec(async () => {\n            await this._applyCommands([[x2ManyCommands.UNLINK, resId, serverData]]);\n            await this._onUpdate();\n        });\n    }\n\n    load({ limit, offset, orderBy } = {}) {\n        return this.model.mutex.exec(async () => {\n            const editedRecord = this.editedRecord;\n            if (editedRecord && !(await editedRecord.checkValidity())) {\n                return;\n            }\n            limit = limit !== undefined ? limit : this.limit;\n            offset = offset !== undefined ? offset : this.offset;\n            orderBy = orderBy !== undefined ? orderBy : this.orderBy;\n            return this._load({ limit, offset, orderBy });\n        });\n    }\n\n    moveRecord(dataRecordId, _dataGroupId, refId, _targetGroupId) {\n        return this.resequence(dataRecordId, refId);\n    }\n\n    sortBy(fieldName) {\n        return this.model.mutex.exec(() => this._sortBy(fieldName));\n    }\n\n    async addAndRemove({ add, remove, reload } = {}) {\n        return this.model.mutex.exec(async () => {\n            const commands = [\n                ...(add || []).map((id) => [x2ManyCommands.LINK, id]),\n                ...(remove || []).map((id) => [x2ManyCommands.UNLINK, id]),\n            ];\n            await this._applyCommands(commands, { canAddOverLimit: true, reload });\n            await this._onUpdate();\n        });\n    }\n\n    async resequence(movedId, targetId) {\n        return this.model.mutex.exec(() => this._resequence(movedId, targetId));\n    }\n\n    /**\n     * This method is meant to be called when a record, which has previously been extended to be\n     * displayed in a form view dialog (see @extendRecord) is saved. In this case, we may need to\n     * add this record to the list (if it is a new one), and to notify the parent record of the\n     * update. We may also want to sort the list.\n     *\n     * @param {Record} record\n     */\n    validateExtendedRecord(record) {\n        return this.model.mutex.exec(async () => {\n            if (!this._currentIds.includes(record.isNew ? record._virtualId : record.resId)) {\n                // new record created, not yet in the list\n                await this._addRecord(record);\n            } else if (!record.dirty) {\n                return;\n            }\n            await this._onUpdate();\n            record._restoreActiveFields();\n            record._savePoint = undefined;\n        });\n    }\n\n    // -------------------------------------------------------------------------\n    // Protected\n    // -------------------------------------------------------------------------\n\n    _abandonRecords(records = this.records, { force } = {}) {\n        for (const record of records) {\n            if (record.canBeAbandoned && (force || !record._checkValidity())) {\n                const virtualId = record._virtualId;\n                const index = this._currentIds.findIndex((id) => id === virtualId);\n                this._currentIds.splice(index, 1);\n                this.records.splice(\n                    this.records.findIndex((r) => r === record),\n                    1\n                );\n                this._commands = this._commands.filter((c) => c[1] !== virtualId);\n                this.count--;\n                if (this._tmpIncreaseLimit > 0) {\n                    this.model._updateConfig(\n                        this.config,\n                        { limit: this.limit - 1 },\n                        { reload: false }\n                    );\n                    this._tmpIncreaseLimit--;\n                }\n            }\n        }\n    }\n\n    async _addRecord(record, { position } = {}) {\n        const command = [x2ManyCommands.CREATE, record._virtualId];\n        if (position === \"top\") {\n            this.records.unshift(record);\n            if (this.records.length > this.limit) {\n                this.records.pop();\n            }\n            this._currentIds.splice(this.offset, 0, record._virtualId);\n            this._commands.unshift(command);\n        } else if (position === \"bottom\") {\n            this.records.push(record);\n            this._currentIds.splice(this.offset + this.limit, 0, record._virtualId);\n            if (this.records.length > this.limit) {\n                this._tmpIncreaseLimit++;\n                const nextLimit = this.limit + 1;\n                this.model._updateConfig(this.config, { limit: nextLimit }, { reload: false });\n            }\n            this._commands.push(command);\n        } else {\n            const currentIds = [...this._currentIds, record._virtualId];\n            if (this.orderBy.length) {\n                await this._sort(currentIds);\n            } else {\n                if (this.records.length < this.limit) {\n                    this.records.push(record);\n                }\n            }\n            this._currentIds = currentIds;\n            this._commands.push(command);\n        }\n        this.count++;\n        this._needsReordering = true;\n    }\n\n    _addSavePoint() {\n        for (const id in this._cache) {\n            this._cache[id]._addSavePoint();\n        }\n        this._savePoint = markRaw({\n            _commands: [...this._commands],\n            _currentIds: [...this._currentIds],\n            count: this.count,\n        });\n    }\n\n    _applyCommands(commands, { canAddOverLimit, reload } = {}) {\n        const { CREATE, UPDATE, DELETE, UNLINK, LINK, SET } = x2ManyCommands;\n\n        // For performance reasons, we split commands by record ids, such that we have quick access\n        // to all commands concerning a given record. At the end, we re-build the list of commands\n        // from this structure.\n        let lastCommandIndex = -1;\n        const commandsByIds = {};\n        function addOwnCommand(command) {\n            commandsByIds[command[1]] = commandsByIds[command[1]] || [];\n            commandsByIds[command[1]].push({ command, index: ++lastCommandIndex });\n        }\n        function getOwnCommands(id) {\n            commandsByIds[id] = commandsByIds[id] || [];\n            return commandsByIds[id];\n        }\n        for (const command of this._commands) {\n            addOwnCommand(command);\n        }\n\n        // For performance reasons, we accumulate removed ids (commands DELETE and UNLINK), and at\n        // the end, we filter once this.records and this._currentIds to remove them.\n        const removedIds = {};\n        const recordsToLoad = [];\n        for (const command of commands) {\n            switch (command[0]) {\n                case CREATE: {\n                    const virtualId = getId(\"virtual\");\n                    const record = this._createRecordDatapoint(command[2], { virtualId });\n                    this.records.push(record);\n                    addOwnCommand([CREATE, virtualId]);\n                    const index = this.offset + this.limit + this._tmpIncreaseLimit;\n                    this._currentIds.splice(index, 0, virtualId);\n                    this._tmpIncreaseLimit = Math.max(this.records.length - this.limit, 0);\n                    const nextLimit = this.limit + this._tmpIncreaseLimit;\n                    this.model._updateConfig(this.config, { limit: nextLimit }, { reload: false });\n                    this.count++;\n                    break;\n                }\n                case UPDATE: {\n                    const existingCommand = getOwnCommands(command[1]).some(\n                        (x) => x.command[0] === CREATE || x.command[0] === UPDATE\n                    );\n                    if (!existingCommand) {\n                        addOwnCommand([UPDATE, command[1]]);\n                    }\n                    const record = this._cache[command[1]];\n                    if (!record) {\n                        // the record isn't in the cache, it means it is on a page we haven't loaded\n                        // so we say the record is \"unknown\", and store all update commands we\n                        // receive about it in a separated structure, s.t. we can easily apply them\n                        // later on after loading the record, if we ever load it.\n                        if (!(command[1] in this._unknownRecordCommands)) {\n                            this._unknownRecordCommands[command[1]] = [];\n                        }\n                        this._unknownRecordCommands[command[1]].push(command);\n                    } else if (command[1] in this._unknownRecordCommands) {\n                        // this case is more tricky: the record is in the cache, but it isn't loaded\n                        // yet, as we are currently loading it (see below, where we load missing\n                        // records for the current page)\n                        this._unknownRecordCommands[command[1]].push(command);\n                    } else {\n                        const changes = {};\n                        for (const fieldName in command[2]) {\n                            if ([\"one2many\", \"many2many\"].includes(this.fields[fieldName].type)) {\n                                const invisible = record.activeFields[fieldName]?.invisible;\n                                if (\n                                    invisible === \"True\" ||\n                                    invisible === \"1\" ||\n                                    !(fieldName in record.activeFields) // this record hasn't been extended\n                                ) {\n                                    if (!(command[1] in this._unknownRecordCommands)) {\n                                        this._unknownRecordCommands[command[1]] = [];\n                                    }\n                                    this._unknownRecordCommands[command[1]].push(command);\n                                    continue;\n                                }\n                            }\n                            changes[fieldName] = command[2][fieldName];\n                        }\n                        record._applyChanges(record._parseServerValues(changes, record.data));\n                    }\n                    break;\n                }\n                case DELETE:\n                case UNLINK: {\n                    // If we receive an UNLINK command and we already have a SET command\n                    // containing the record to unlink, we just remove it from the SET command.\n                    // If there's a SET command, we know it's the first one (see @_replaceWith).\n                    if (command[0] === UNLINK) {\n                        const firstCommand = this._commands[0];\n                        const hasReplaceWithCommand = firstCommand && firstCommand[0] === SET;\n                        if (hasReplaceWithCommand && firstCommand[2].includes(command[1])) {\n                            firstCommand[2] = firstCommand[2].filter((id) => id !== command[1]);\n                            break;\n                        }\n                    }\n                    const ownCommands = getOwnCommands(command[1]);\n                    if (command[0] === DELETE) {\n                        const hasCreateCommand = ownCommands.some((x) => x.command[0] === CREATE);\n                        ownCommands.splice(0); // reset to the empty list\n                        if (!hasCreateCommand) {\n                            addOwnCommand([DELETE, command[1]]);\n                        }\n                    } else {\n                        const linkToIndex = ownCommands.findIndex((x) => x.command[0] === LINK);\n                        if (linkToIndex >= 0) {\n                            ownCommands.splice(linkToIndex, 1);\n                        } else {\n                            addOwnCommand([UNLINK, command[1]]);\n                        }\n                    }\n                    removedIds[command[1]] = true;\n                    break;\n                }\n                case LINK: {\n                    let record;\n                    if (command[1] in this._cache) {\n                        record = this._cache[command[1]];\n                    } else {\n                        record = this._createRecordDatapoint({ ...command[2], id: command[1] });\n                    }\n                    if (!this.limit || this.records.length < this.limit || canAddOverLimit) {\n                        if (!command[2]) {\n                            recordsToLoad.push(record);\n                        }\n                        this.records.push(record);\n                        if (this.records.length > this.limit) {\n                            this._tmpIncreaseLimit = this.records.length - this.limit;\n                            const nextLimit = this.limit + this._tmpIncreaseLimit;\n                            this.model._updateConfig(\n                                this.config,\n                                { limit: nextLimit },\n                                { reload: false }\n                            );\n                        }\n                    }\n                    this._currentIds.push(record.resId);\n                    addOwnCommand([command[0], command[1]]);\n                    this.count++;\n                    break;\n                }\n            }\n        }\n\n        // Re-generate the new list of commands\n        this._commands = Object.values(commandsByIds)\n            .flat()\n            .sort((x, y) => x.index - y.index)\n            .map((x) => x.command);\n\n        // Filter out removed records and ids from this.records and this._currentIds\n        if (Object.keys(removedIds).length) {\n            let removeCommandsByIdsCopy = Object.assign({}, removedIds);\n            this.records = this.records.filter((r) => {\n                const id = r.resId || r._virtualId;\n                if (removeCommandsByIdsCopy[id]) {\n                    delete removeCommandsByIdsCopy[id];\n                    return false;\n                }\n                return true;\n            });\n            const nextCurrentIds = [];\n            removeCommandsByIdsCopy = Object.assign({}, removedIds);\n            for (const id of this._currentIds) {\n                if (removeCommandsByIdsCopy[id]) {\n                    delete removeCommandsByIdsCopy[id];\n                } else {\n                    nextCurrentIds.push(id);\n                }\n            }\n            this._currentIds = nextCurrentIds;\n            this.count = this._currentIds.length;\n        }\n\n        // Fill the page if it isn't full w.r.t. the limit. This may happen if we aren't on the last\n        // page and records of the current have been removed, or if we applied commands to remove\n        // some records and to add others, but we were on the limit.\n        const nbMissingRecords = this.limit - this.records.length;\n        if (nbMissingRecords > 0) {\n            const lastRecordIndex = this.limit + this.offset;\n            const firstRecordIndex = lastRecordIndex - nbMissingRecords;\n            const nextRecordIds = this._currentIds.slice(firstRecordIndex, lastRecordIndex);\n            for (const id of this._getResIdsToLoad(nextRecordIds)) {\n                const record = this._createRecordDatapoint({ id }, { dontApplyCommands: true });\n                recordsToLoad.push(record);\n            }\n            for (const id of nextRecordIds) {\n                this.records.push(this._cache[id]);\n            }\n        }\n        if (recordsToLoad.length || reload) {\n            const resIds = reload\n                ? this.records.map((r) => r.resId)\n                : recordsToLoad.map((r) => r.resId);\n            return this.model._loadRecords({ ...this.config, resIds }).then((recordValues) => {\n                if (reload) {\n                    for (const record of recordValues) {\n                        this._createRecordDatapoint(record);\n                    }\n                    this.records = resIds.map((id) => this._cache[id]);\n                    return;\n                }\n                for (let i = 0; i < recordsToLoad.length; i++) {\n                    const record = recordsToLoad[i];\n                    record._applyValues(recordValues[i]);\n                    const commands = this._unknownRecordCommands[record.resId];\n                    if (commands) {\n                        delete this._unknownRecordCommands[record.resId];\n                        this._applyCommands(commands);\n                    }\n                }\n            });\n        }\n    }\n\n    _applyInitialCommands(commands) {\n        this._applyCommands(commands);\n        this._initialCommands = [...commands];\n        this._initialCurrentIds = [...this._currentIds];\n    }\n\n    async _createNewRecordDatapoint(params = {}) {\n        const changes = {};\n        if (!params.withoutParent && this.config.relationField) {\n            changes[this.config.relationField] = this._parent._getChanges();\n            if (!this._parent.isNew) {\n                changes[this.config.relationField].id = this._parent.resId;\n            }\n        }\n        const values = await this.model._loadNewRecord(\n            {\n                resModel: this.resModel,\n                activeFields: params.activeFields || this.activeFields,\n                fields: this.fields,\n                context: Object.assign({}, this.context, params.context),\n            },\n            { changes, evalContext: this.evalContext }\n        );\n\n        if (this.canResequence() && this.records.length) {\n            const position = params.position || \"bottom\";\n            const order = this.orderBy[0];\n            const asc = !order || order.asc;\n            let value;\n            if (position === \"top\") {\n                const isOnFirstPage = this.offset === 0;\n                value = this.records[0].data[this.handleField];\n                if (isOnFirstPage) {\n                    if (asc) {\n                        value = value > 0 ? value - 1 : 0;\n                    } else {\n                        value = value + 1;\n                    }\n                }\n            } else if (position === \"bottom\") {\n                value = this.records[this.records.length - 1].data[this.handleField];\n                const isOnLastPage = this.limit + this.offset >= this.count;\n                if (isOnLastPage) {\n                    if (asc) {\n                        value = value + 1;\n                    } else {\n                        value = value > 0 ? value - 1 : 0;\n                    }\n                }\n            }\n            values[this.handleField] = value;\n        }\n        return this._createRecordDatapoint(values, {\n            mode: params.mode || \"edit\",\n            virtualId: getId(\"virtual\"),\n            activeFields: params.activeFields,\n            manuallyAdded: params.manuallyAdded,\n        });\n    }\n\n    _createRecordDatapoint(data, params = {}) {\n        const resId = data.id || false;\n        if (!resId && !params.virtualId) {\n            throw new Error(\"You must provide a virtualId if the record has no id\");\n        }\n        const id = resId || params.virtualId;\n        const config = {\n            context: this.context,\n            activeFields: Object.assign({}, params.activeFields || this.activeFields),\n            resModel: this.resModel,\n            fields: params.fields || this.fields,\n            relationField: this.config.relationField,\n            resId,\n            resIds: resId ? [resId] : [],\n            mode: params.mode || \"readonly\",\n            isMonoRecord: true,\n            currentCompanyId: this.currentCompanyId,\n        };\n        const { CREATE, UPDATE } = x2ManyCommands;\n        const options = {\n            parentRecord: this._parent,\n            onUpdate: async ({ withoutParentUpdate }) => {\n                const id = record.isNew ? record._virtualId : record.resId;\n                if (!this.currentIds.includes(id)) {\n                    // the record hasn't been added to the list yet (we're currently creating it\n                    // from a dialog)\n                    return;\n                }\n                const hasCommand = this._commands.some(\n                    (c) => (c[0] === CREATE || c[0] === UPDATE) && c[1] === id\n                );\n                if (!hasCommand) {\n                    this._commands.push([UPDATE, id]);\n                }\n                if (record._noUpdateParent) {\n                    // the record is edited from a dialog, so we don't want to notify the parent\n                    // record to be notified at each change inside the dialog (it will be notified\n                    // at the end when the dialog is saved)\n                    return;\n                }\n                if (!withoutParentUpdate) {\n                    await this._onUpdate({\n                        withoutOnchange: !record._checkValidity({ silent: true }),\n                    });\n                }\n            },\n            virtualId: params.virtualId,\n            manuallyAdded: params.manuallyAdded,\n        };\n        const record = new this.model.constructor.Record(this.model, config, data, options);\n        this._cache[id] = record;\n        if (!params.dontApplyCommands) {\n            const commands = this._unknownRecordCommands[id];\n            if (commands) {\n                delete this._unknownRecordCommands[id];\n                this._applyCommands(commands);\n            }\n        }\n        return record;\n    }\n\n    _clearCommands() {\n        this._commands = [];\n        this._unknownRecordCommands = {};\n    }\n\n    _discard() {\n        for (const id in this._cache) {\n            this._cache[id]._discard();\n        }\n        if (this._savePoint) {\n            this._commands = this._savePoint._commands;\n            this._currentIds = this._savePoint._currentIds;\n            this.count = this._savePoint.count;\n        } else {\n            this._commands = [];\n            this._currentIds = [...this.resIds];\n            this.count = this.resIds.length;\n        }\n        this._unknownRecordCommands = {};\n        const limit = this.limit - this._tmpIncreaseLimit;\n        this._tmpIncreaseLimit = 0;\n        this.model._updateConfig(this.config, { limit }, { reload: false });\n        this.records = this._currentIds\n            .slice(this.offset, this.limit)\n            .map((resId) => this._cache[resId]);\n        if (!this._savePoint) {\n            this._applyCommands(this._initialCommands);\n        }\n        this._savePoint = undefined;\n    }\n\n    _getCommands({ withReadonly } = {}) {\n        const { CREATE, UPDATE, LINK } = x2ManyCommands;\n        const commands = [];\n        for (const command of this._commands) {\n            if (command[0] === UPDATE && command[1] in this._unknownRecordCommands) {\n                // the record has never been loaded, but we received update commands from the\n                // server for it, so we need to sanitize them (as they contained unity values)\n                const uCommands = this._unknownRecordCommands[command[1]];\n                for (const uCommand of uCommands) {\n                    const values = fromUnityToServerValues(\n                        uCommand[2],\n                        this.fields,\n                        this.activeFields,\n                        { withReadonly, context: this.context }\n                    );\n                    commands.push([uCommand[0], uCommand[1], values]);\n                }\n            } else if (command[0] === CREATE || command[0] === UPDATE) {\n                const record = this._cache[command[1]];\n                if (command[0] === CREATE && record.resId) {\n                    // we created a new record, but it has already been saved (e.g. because we clicked\n                    // on a view button in the x2many dialog), so replace the CREATE command by a\n                    // LINK\n                    commands.push([LINK, record.resId]);\n                } else {\n                    const values = record._getChanges(record._changes, { withReadonly });\n                    if (command[0] === CREATE || Object.keys(values).length) {\n                        commands.push([command[0], command[1], values]);\n                    }\n                }\n            } else {\n                commands.push(command);\n            }\n        }\n        return commands;\n    }\n\n    _getResIdsToLoad(resIds, fieldNames = this.fieldNames) {\n        return resIds.filter((resId) => {\n            if (typeof resId === \"string\") {\n                // this is a virtual id, we don't want to read it\n                return false;\n            }\n            const record = this._cache[resId];\n            if (!record) {\n                // record hasn't been loaded yet\n                return true;\n            }\n            // record has already been loaded -> check if we already read all orderBy fields\n            fieldNames = fieldNames.filter((fieldName) => fieldName !== \"id\");\n            return intersection(fieldNames, record.fieldNames).length !== fieldNames.length;\n        });\n    }\n\n    async _load({\n        limit = this.limit,\n        offset = this.offset,\n        orderBy = this.orderBy,\n        nextCurrentIds = this._currentIds,\n    } = {}) {\n        const currentIds = nextCurrentIds.slice(offset, offset + limit);\n        const resIds = this._getResIdsToLoad(currentIds);\n        if (resIds.length) {\n            const records = await this.model._loadRecords(\n                { ...this.config, resIds },\n                this.evalContext\n            );\n            for (const record of records) {\n                this._createRecordDatapoint(record);\n            }\n        }\n        this.records = currentIds.map((id) => this._cache[id]);\n        this._currentIds = nextCurrentIds;\n        await this.model._updateConfig(this.config, { limit, offset, orderBy }, { reload: false });\n    }\n\n    async _replaceWith(ids, { reload = false } = {}) {\n        const resIds = reload ? ids : ids.filter((id) => !this._cache[id]);\n        if (resIds.length) {\n            const records = await this.model._loadRecords({\n                ...this.config,\n                resIds,\n                context: this.context,\n            });\n            for (const record of records) {\n                this._createRecordDatapoint(record);\n            }\n        }\n        this.records = ids.map((id) => this._cache[id]);\n        const updateCommandsToKeep = this._commands.filter(\n            (c) => c[0] === x2ManyCommands.UPDATE && ids.includes(c[1])\n        );\n        this._commands = [x2ManyCommands.set(ids)].concat(updateCommandsToKeep);\n        this._currentIds = [...ids];\n        this.count = this._currentIds.length;\n        if (this._currentIds.length > this.limit) {\n            this._tmpIncreaseLimit = this._currentIds.length - this.limit;\n            const nextLimit = this.limit + this._tmpIncreaseLimit;\n            this.model._updateConfig(this.config, { limit: nextLimit }, { reload: false });\n        }\n    }\n\n    async _resequence(movedId, targetId) {\n        const records = [...this.records];\n        const order = this.orderBy.find((o) => o.name === this.handleField);\n        const asc = !order || order.asc;\n\n        // Find indices\n        const fromIndex = records.findIndex((r) => r.id === movedId);\n        let toIndex = 0;\n        if (targetId !== null) {\n            const targetIndex = records.findIndex((r) => r.id === targetId);\n            toIndex = fromIndex > targetIndex ? targetIndex + 1 : targetIndex;\n        }\n\n        const getSequence = (rec) => rec && rec.data[this.handleField];\n\n        // Determine what records need to be modified\n        const firstIndex = Math.min(fromIndex, toIndex);\n        const lastIndex = Math.max(fromIndex, toIndex) + 1;\n        let reorderAll = false;\n        let lastSequence = (asc ? -1 : 1) * Infinity;\n        for (let index = 0; index < records.length; index++) {\n            const sequence = getSequence(records[index]);\n            if ((asc && lastSequence >= sequence) || (!asc && lastSequence <= sequence)) {\n                reorderAll = true;\n                break;\n            }\n            lastSequence = sequence;\n        }\n\n        // Perform the resequence in the list of records\n        const [record] = records.splice(fromIndex, 1);\n        records.splice(toIndex, 0, record);\n\n        // Creates the list of to modify\n        let toReorder = records;\n        if (!reorderAll) {\n            toReorder = toReorder.slice(firstIndex, lastIndex).filter((r) => r.id !== movedId);\n            if (fromIndex < toIndex) {\n                toReorder.push(record);\n            } else {\n                toReorder.unshift(record);\n            }\n        }\n        if (!asc) {\n            toReorder.reverse();\n        }\n\n        const sequences = toReorder.map(getSequence);\n        const offset = sequences.length && Math.min(...sequences);\n\n        const proms = [];\n        for (const [i, record] of Object.entries(toReorder)) {\n            proms.push(\n                record._update(\n                    { [this.handleField]: offset + Number(i) },\n                    { withoutParentUpdate: true }\n                )\n            );\n        }\n        await Promise.all(proms);\n\n        await this._sort();\n        await this._onUpdate();\n    }\n\n    async _sort(currentIds = this.currentIds, orderBy = this.orderBy) {\n        const fieldNames = orderBy.map((o) => o.name);\n        const resIds = this._getResIdsToLoad(currentIds, fieldNames);\n        if (resIds.length) {\n            const activeFields = pick(this.activeFields, ...fieldNames);\n            const config = { ...this.config, resIds, activeFields };\n            const records = await this.model._loadRecords(config);\n            for (const record of records) {\n                this._createRecordDatapoint(record, { activeFields });\n            }\n        }\n        const allRecords = currentIds.map((id) => this._cache[id]);\n        const sortedRecords = allRecords.sort((r1, r2) => {\n            return compareRecords(r1, r2, orderBy, this.fields);\n        });\n        await this._load({\n            orderBy,\n            nextCurrentIds: sortedRecords.map((r) => r.resId || r._virtualId),\n        });\n        this._needsReordering = false;\n    }\n\n    async _sortBy(fieldName) {\n        let orderBy = [...this.orderBy];\n        if (fieldName) {\n            if (orderBy.length && orderBy[0].name === fieldName) {\n                if (!this._needsReordering) {\n                    orderBy[0] = { name: orderBy[0].name, asc: !orderBy[0].asc };\n                }\n            } else {\n                orderBy = orderBy.filter((o) => o.name !== fieldName);\n                orderBy.unshift({\n                    name: fieldName,\n                    asc: true,\n                });\n            }\n        }\n        return this._sort(this._currentIds, orderBy);\n    }\n\n    _updateContext(context) {\n        Object.assign(this.context, context);\n        for (const record of Object.values(this._cache)) {\n            record._setEvalContext();\n        }\n    }\n}\n", "import { markup, onWillDestroy, onWillStart, onWillUpdateProps, useComponent } from \"@odoo/owl\";\nimport { evalPartialContext, makeContext } from \"@web/core/context\";\nimport { Domain } from \"@web/core/domain\";\nimport {\n    deserializeDate,\n    deserializeDateTime,\n    serializeDate,\n    serializeDateTime,\n} from \"@web/core/l10n/dates\";\nimport { x2ManyCommands } from \"@web/core/orm_service\";\nimport { evaluateExpr } from \"@web/core/py_js/py\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { omit } from \"@web/core/utils/objects\";\nimport { effect } from \"@web/core/utils/reactive\";\nimport { batched } from \"@web/core/utils/timing\";\nimport { orderByToString } from \"@web/search/utils/order_by\";\nimport { rpc } from \"@web/core/network/rpc\";\n\n/**\n * @param {boolean || string} value boolean or string encoding a python expression\n * @returns {string} string encoding a python expression\n */\nfunction convertBoolToPyExpr(value) {\n    if (value === true || value === false) {\n        return value ? \"True\" : \"False\";\n    }\n    return value;\n}\n\nexport function makeActiveField({\n    context,\n    invisible,\n    readonly,\n    required,\n    onChange,\n    forceSave,\n    isHandle,\n} = {}) {\n    return {\n        context: context || \"{}\",\n        invisible: convertBoolToPyExpr(invisible || false),\n        readonly: convertBoolToPyExpr(readonly || false),\n        required: convertBoolToPyExpr(required || false),\n        onChange: onChange || false,\n        forceSave: forceSave || false,\n        isHandle: isHandle || false,\n    };\n}\n\nconst AGGREGATABLE_FIELD_TYPES = [\"float\", \"integer\", \"monetary\"]; // types that can be aggregated in grouped views\n\nexport function addFieldDependencies(activeFields, fields, fieldDependencies = []) {\n    for (const field of fieldDependencies) {\n        if (!(\"readonly\" in field)) {\n            field.readonly = true;\n        }\n        if (field.name in activeFields) {\n            patchActiveFields(activeFields[field.name], makeActiveField(field));\n        } else {\n            activeFields[field.name] = makeActiveField(field);\n        }\n        if (!fields[field.name]) {\n            const newField = omit(field, [\n                \"context\",\n                \"invisible\",\n                \"required\",\n                \"readonly\",\n                \"onChange\",\n            ]);\n            fields[field.name] = newField;\n            if (newField.type === \"selection\" && !Array.isArray(newField.selection)) {\n                newField.selection = [];\n            }\n        }\n    }\n}\n\nfunction completeActiveField(activeField, extra) {\n    if (extra.related) {\n        for (const fieldName in extra.related.activeFields) {\n            if (fieldName in activeField.related.activeFields) {\n                completeActiveField(\n                    activeField.related.activeFields[fieldName],\n                    extra.related.activeFields[fieldName]\n                );\n            } else {\n                activeField.related.activeFields[fieldName] = {\n                    ...extra.related.activeFields[fieldName],\n                };\n            }\n        }\n        Object.assign(activeField.related.fields, extra.related.fields);\n    }\n}\n\nexport function completeActiveFields(activeFields, extraActiveFields) {\n    for (const fieldName in extraActiveFields) {\n        const extraActiveField = {\n            ...extraActiveFields[fieldName],\n            invisible: \"True\",\n        };\n        if (fieldName in activeFields) {\n            completeActiveField(activeFields[fieldName], extraActiveField);\n        } else {\n            activeFields[fieldName] = extraActiveField;\n        }\n    }\n}\n\nexport function createPropertyActiveField(property) {\n    const { type } = property;\n\n    const activeField = makeActiveField();\n    if (type === \"one2many\" || type === \"many2many\") {\n        activeField.related = {\n            fields: {\n                id: { name: \"id\", type: \"integer\" },\n                display_name: { name: \"display_name\", type: \"char\" },\n            },\n            activeFields: {\n                id: makeActiveField({ readonly: true }),\n                display_name: makeActiveField(),\n            },\n        };\n    }\n    return activeField;\n}\n\nexport function combineModifiers(mod1, mod2, operator) {\n    if (operator === \"AND\") {\n        if (!mod1 || mod1 === \"False\" || !mod2 || mod2 === \"False\") {\n            return \"False\";\n        }\n        if (mod1 === \"True\") {\n            return mod2;\n        }\n        if (mod2 === \"True\") {\n            return mod1;\n        }\n        return \"(\" + mod1 + \") and (\" + mod2 + \")\";\n    } else if (operator === \"OR\") {\n        if (mod1 === \"True\" || mod2 === \"True\") {\n            return \"True\";\n        }\n        if (!mod1 || mod1 === \"False\") {\n            return mod2;\n        }\n        if (!mod2 || mod2 === \"False\") {\n            return mod1;\n        }\n        return \"(\" + mod1 + \") or (\" + mod2 + \")\";\n    }\n    throw new Error(\n        `Operator provided to \"combineModifiers\" must be \"AND\" or \"OR\", received ${operator}`\n    );\n}\n\nexport function patchActiveFields(activeField, patch) {\n    activeField.invisible = combineModifiers(activeField.invisible, patch.invisible, \"AND\");\n    activeField.readonly = combineModifiers(activeField.readonly, patch.readonly, \"AND\");\n    activeField.required = combineModifiers(activeField.required, patch.required, \"OR\");\n    activeField.onChange = activeField.onChange || patch.onChange;\n    activeField.forceSave = activeField.forceSave || patch.forceSave;\n    activeField.isHandle = activeField.isHandle || patch.isHandle;\n    // x2manys\n    if (patch.related) {\n        const related = activeField.related;\n        for (const fieldName in patch.related.activeFields) {\n            if (fieldName in related.activeFields) {\n                patchActiveFields(\n                    related.activeFields[fieldName],\n                    patch.related.activeFields[fieldName]\n                );\n            } else {\n                related.activeFields[fieldName] = { ...patch.related.activeFields[fieldName] };\n            }\n        }\n        Object.assign(related.fields, patch.related.fields);\n    }\n    if (\"limit\" in patch) {\n        activeField.limit = patch.limit;\n    }\n    if (patch.defaultOrderBy) {\n        activeField.defaultOrderBy = patch.defaultOrderBy;\n    }\n}\n\nexport function extractFieldsFromArchInfo({ fieldNodes, widgetNodes }, fields) {\n    const activeFields = {};\n    for (const fieldNode of Object.values(fieldNodes)) {\n        const fieldName = fieldNode.name;\n        const activeField = makeActiveField({\n            context: fieldNode.context,\n            invisible: combineModifiers(fieldNode.invisible, fieldNode.column_invisible, \"OR\"),\n            readonly: fieldNode.readonly,\n            required: fieldNode.required,\n            onChange: fieldNode.onChange,\n            forceSave: fieldNode.forceSave,\n            isHandle: fieldNode.isHandle,\n        });\n        if ([\"one2many\", \"many2many\"].includes(fields[fieldName].type)) {\n            activeField.related = {\n                activeFields: {},\n                fields: {},\n            };\n            if (fieldNode.views) {\n                const viewDescr = fieldNode.views[fieldNode.viewMode];\n                if (viewDescr) {\n                    activeField.related = extractFieldsFromArchInfo(viewDescr, viewDescr.fields);\n                    activeField.limit = viewDescr.limit;\n                    activeField.defaultOrderBy = viewDescr.defaultOrder;\n                    if (fieldNode.views.form) {\n                        // we already know the form view (it is inline), so add its fields (in invisible)\n                        // s.t. they will be sent in the spec for onchange, and create commands returned\n                        // by the onchange could return values for those fields (that would be displayed\n                        // later if the user opens the form view)\n                        const formArchInfo = extractFieldsFromArchInfo(\n                            fieldNode.views.form,\n                            fieldNode.views.form.fields\n                        );\n                        completeActiveFields(\n                            activeField.related.activeFields,\n                            formArchInfo.activeFields\n                        );\n                        Object.assign(activeField.related.fields, formArchInfo.fields);\n                    }\n\n                    if (fieldNode.viewMode !== \"default\" && fieldNode.views.default) {\n                        const defaultArchInfo = extractFieldsFromArchInfo(\n                            fieldNode.views.default,\n                            fieldNode.views.default.fields\n                        );\n                        for (const fieldName in defaultArchInfo.activeFields) {\n                            if (fieldName in activeField.related.activeFields) {\n                                patchActiveFields(\n                                    activeField.related.activeFields[fieldName],\n                                    defaultArchInfo.activeFields[fieldName]\n                                );\n                            } else {\n                                activeField.related.activeFields[fieldName] = {\n                                    ...defaultArchInfo.activeFields[fieldName],\n                                };\n                            }\n                        }\n                        activeField.related.fields = Object.assign(\n                            {},\n                            defaultArchInfo.fields,\n                            activeField.related.fields\n                        );\n                    }\n                }\n            }\n            if (fieldNode.field?.useSubView) {\n                activeField.required = \"False\";\n            }\n        }\n        if (fields[fieldName].type === \"many2one_reference\" && fieldNode.views) {\n            const viewDescr = fieldNode.views.default;\n            activeField.related = extractFieldsFromArchInfo(viewDescr, viewDescr.fields);\n        }\n\n        if (fieldName in activeFields) {\n            patchActiveFields(activeFields[fieldName], activeField);\n        } else {\n            activeFields[fieldName] = activeField;\n        }\n\n        if (fieldNode.field) {\n            let fieldDependencies = fieldNode.field.fieldDependencies;\n            if (typeof fieldDependencies === \"function\") {\n                fieldDependencies = fieldDependencies(fieldNode);\n            }\n            addFieldDependencies(activeFields, fields, fieldDependencies);\n        }\n    }\n\n    for (const widgetInfo of Object.values(widgetNodes || {})) {\n        let fieldDependencies = widgetInfo.widget.fieldDependencies;\n        if (typeof fieldDependencies === \"function\") {\n            fieldDependencies = fieldDependencies(widgetInfo);\n        }\n        addFieldDependencies(activeFields, fields, fieldDependencies);\n    }\n    return { activeFields, fields };\n}\n\nexport function getFieldContext(\n    record,\n    fieldName,\n    rawContext = record.activeFields[fieldName].context\n) {\n    const context = {};\n    for (const key in record.context) {\n        if (\n            !key.startsWith(\"default_\") &&\n            !key.startsWith(\"search_default_\") &&\n            !key.endsWith(\"_view_ref\")\n        ) {\n            context[key] = record.context[key];\n        }\n    }\n\n    return {\n        ...context,\n        ...record.fields[fieldName].context,\n        ...makeContext([rawContext], record.evalContext),\n    };\n}\n\nexport function getFieldDomain(record, fieldName, domain) {\n    if (typeof domain === \"function\") {\n        domain = domain();\n        domain = typeof domain === \"function\" ? domain() : domain;\n    }\n    if (domain) {\n        return domain;\n    }\n    // Fallback to the domain defined in the field definition in python\n    domain = record.fields[fieldName].domain;\n    return typeof domain === \"string\"\n        ? new Domain(evaluateExpr(domain, record.evalContext)).toList()\n        : domain || [];\n}\n\nexport function getBasicEvalContext(config) {\n    const { uid, allowed_company_ids } = config.context;\n    return {\n        context: config.context,\n        uid,\n        allowed_company_ids,\n        current_company_id: config.currentCompanyId,\n    };\n}\n\nfunction getFieldContextForSpec(activeFields, fields, fieldName, evalContext) {\n    let context = activeFields[fieldName].context;\n    if (!context || context === \"{}\") {\n        context = fields[fieldName].context || {};\n    } else {\n        context = evalPartialContext(context, evalContext);\n    }\n    if (Object.keys(context).length > 0) {\n        return context;\n    }\n}\n\nexport function getFieldsSpec(activeFields, fields, evalContext, { withInvisible } = {}) {\n    const fieldsSpec = {};\n    const properties = [];\n    for (const fieldName in activeFields) {\n        if (fields[fieldName].relatedPropertyField) {\n            continue;\n        }\n        const { related, limit, defaultOrderBy, invisible } = activeFields[fieldName];\n        const isAlwaysInvisible = invisible === \"True\" || invisible === \"1\";\n        fieldsSpec[fieldName] = {};\n        switch (fields[fieldName].type) {\n            case \"one2many\":\n            case \"many2many\": {\n                if (related && (withInvisible || !isAlwaysInvisible)) {\n                    fieldsSpec[fieldName].fields = getFieldsSpec(\n                        related.activeFields,\n                        related.fields,\n                        evalContext,\n                        { withInvisible }\n                    );\n                    fieldsSpec[fieldName].context = getFieldContextForSpec(\n                        activeFields,\n                        fields,\n                        fieldName,\n                        evalContext\n                    );\n                    fieldsSpec[fieldName].limit = limit;\n                    if (defaultOrderBy) {\n                        fieldsSpec[fieldName].order = orderByToString(defaultOrderBy);\n                    }\n                }\n                break;\n            }\n            case \"many2one\":\n            case \"reference\": {\n                fieldsSpec[fieldName].fields = {};\n                if (!isAlwaysInvisible) {\n                    fieldsSpec[fieldName].fields.display_name = {};\n                    fieldsSpec[fieldName].context = getFieldContextForSpec(\n                        activeFields,\n                        fields,\n                        fieldName,\n                        evalContext\n                    );\n                }\n                break;\n            }\n            case \"many2one_reference\": {\n                if (related && !isAlwaysInvisible) {\n                    fieldsSpec[fieldName].fields = getFieldsSpec(\n                        related.activeFields,\n                        related.fields,\n                        evalContext\n                    );\n                    fieldsSpec[fieldName].context = getFieldContextForSpec(\n                        activeFields,\n                        fields,\n                        fieldName,\n                        evalContext\n                    );\n                }\n                break;\n            }\n            case \"properties\": {\n                properties.push(fieldName);\n                break;\n            }\n        }\n    }\n\n    for (const fieldName of properties) {\n        const fieldSpec = fieldsSpec[fields[fieldName].definition_record];\n        if (fieldSpec) {\n            if (!fieldSpec.fields) {\n                fieldSpec.fields = {};\n            }\n            fieldSpec.fields.display_name = {};\n        }\n    }\n    return fieldsSpec;\n}\n\nlet nextId = 0;\n/**\n * @param {string} [prefix]\n * @returns {string}\n */\nexport function getId(prefix = \"\") {\n    return `${prefix}_${++nextId}`;\n}\n\n/**\n * @protected\n * @param {Field | false} field\n * @param {any} value\n * @returns {any}\n */\nexport function parseServerValue(field, value) {\n    switch (field.type) {\n        case \"char\":\n        case \"text\": {\n            return value || \"\";\n        }\n        case \"html\": {\n            return markup(value || \"\");\n        }\n        case \"date\": {\n            return value ? deserializeDate(value) : false;\n        }\n        case \"datetime\": {\n            return value ? deserializeDateTime(value) : false;\n        }\n        case \"selection\": {\n            if (value === false) {\n                // process selection: convert false to 0, if 0 is a valid key\n                const hasKey0 = field.selection.find((option) => option[0] === 0);\n                return hasKey0 ? 0 : value;\n            }\n            return value;\n        }\n        case \"reference\": {\n            if (value === false) {\n                return false;\n            }\n            return {\n                resId: value.id.id,\n                resModel: value.id.model,\n                displayName: value.display_name,\n            };\n        }\n        case \"many2one_reference\": {\n            if (value === 0) {\n                // unset many2one_reference fields' value is 0\n                return false;\n            }\n            if (typeof value === \"number\") {\n                // many2one_reference fetched without \"fields\" key in spec -> only returns the id\n                return { resId: value };\n            }\n            return {\n                resId: value.id,\n                displayName: value.display_name,\n            };\n        }\n        case \"many2one\": {\n            if (Array.isArray(value)) {\n                // Used for web_read_group, where the value is an array of [id, display_name]\n                return value;\n            }\n            return value ? [value.id, value.display_name] : false;\n        }\n        case \"properties\": {\n            return value\n                ? value.map((property) => ({\n                      ...property,\n                      value: parseServerValue(property, property.value ?? false),\n                  }))\n                : [];\n        }\n    }\n    return value;\n}\n\n/**\n * Extract useful information from a group data returned by a call to webReadGroup.\n *\n * @param {Object} groupData\n * @param {string[]} groupBy\n * @param {Object} fields\n * @returns {Object}\n */\nexport function extractInfoFromGroupData(groupData, groupBy, fields) {\n    const info = {};\n    const groupByField = fields[groupBy[0].split(\":\")[0]];\n    // sometimes the key FIELD_ID_count doesn't exist and we have to get the count from `__count` instead\n    // see read_group in models.py\n    info.count = groupData.__count || groupData[`${groupByField.name}_count`];\n    info.length = info.count; // TODO: remove\n    info.range = groupData.__range ? groupData.__range[groupBy[0]] : null;\n    info.domain = groupData.__domain;\n    info.rawValue = groupData[groupBy[0]];\n    info.value = getValueFromGroupData(groupByField, info.rawValue, info.range);\n    info.displayName = getDisplayNameFromGroupData(groupByField, info.rawValue);\n    info.serverValue = getGroupServerValue(groupByField, info.value);\n    info.aggregates = getAggregatesFromGroupData(groupData, fields);\n    return info;\n}\n\n/**\n * @param {Object} groupData\n * @returns {Object}\n */\nfunction getAggregatesFromGroupData(groupData, fields) {\n    const aggregates = {};\n    for (const [key, value] of Object.entries(groupData)) {\n        if (key in fields && AGGREGATABLE_FIELD_TYPES.includes(fields[key].type)) {\n            aggregates[key] = value;\n        }\n    }\n    return aggregates;\n}\n\n/**\n * @param {import(\"./datapoint\").Field} field\n * @param {any} rawValue\n * @returns {string | false}\n */\nfunction getDisplayNameFromGroupData(field, rawValue) {\n    if (field.type === \"selection\") {\n        return Object.fromEntries(field.selection)[rawValue];\n    }\n    if ([\"many2one\", \"many2many\", \"tags\"].includes(field.type)) {\n        return rawValue ? rawValue[1] : false;\n    }\n    return rawValue;\n}\n\n/**\n * @param {import(\"./datapoint\").Field} field\n * @param {any} value\n * @returns {any}\n */\nexport function getGroupServerValue(field, value) {\n    switch (field.type) {\n        case \"many2many\": {\n            return value ? [value] : false;\n        }\n        case \"datetime\": {\n            return value ? serializeDateTime(value) : false;\n        }\n        case \"date\": {\n            return value ? serializeDate(value) : false;\n        }\n        default: {\n            return value || false;\n        }\n    }\n}\n\n/**\n * @param {import(\"./datapoint\").Field} field\n * @param {any} rawValue\n * @param {object} [range]\n * @returns {any}\n */\nfunction getValueFromGroupData(field, rawValue, range) {\n    if ([\"date\", \"datetime\"].includes(field.type)) {\n        if (!range) {\n            return false;\n        }\n        const dateValue = parseServerValue(field, range.to);\n        return dateValue.minus({\n            [field.type === \"date\" ? \"day\" : \"second\"]: 1,\n        });\n    }\n    const value = parseServerValue(field, rawValue);\n    if ([\"many2one\", \"many2many\"].includes(field.type)) {\n        return value ? value[0] : false;\n    }\n    return value;\n}\n\n/**\n * Onchanges sometimes return update commands for records we don't know (e.g. if\n * they are on a page we haven't loaded yet). We may actually never load them.\n * When this happens, we must still be able to send back those commands to the\n * server when saving. However, we can't send the commands exactly as we received\n * them, since the values they contain have been \"unity read\". The purpose of this\n * function is to transform field values from the unity format to the format\n * expected by the server for a write.\n * For instance, for a many2one: { id: 3, display_name: \"Marc\" } => 3.\n */\nexport function fromUnityToServerValues(\n    values,\n    fields,\n    activeFields,\n    { withReadonly, context } = {}\n) {\n    const { CREATE, UPDATE } = x2ManyCommands;\n    const serverValues = {};\n    for (const fieldName in values) {\n        let value = values[fieldName];\n        const field = fields[fieldName];\n        const activeField = activeFields[fieldName];\n        if (!withReadonly) {\n            if (field.readonly) {\n                continue;\n            }\n            try {\n                if (evaluateExpr(activeField.readonly, context)) {\n                    continue;\n                }\n            } catch {\n                // if the readonly expression depends on other fields, we can't evaluate it as we\n                // didn't read the record, so we simply ignore it\n            }\n        }\n        switch (fields[fieldName].type) {\n            case \"one2many\":\n            case \"many2many\":\n                value = value.map((c) => {\n                    if (c[0] === CREATE || c[0] === UPDATE) {\n                        const _fields = activeField.related.fields;\n                        const _activeFields = activeField.related.activeFields;\n                        return [\n                            c[0],\n                            c[1],\n                            fromUnityToServerValues(c[2], _fields, _activeFields, { withReadonly }),\n                        ];\n                    }\n                    return [c[0], c[1]];\n                });\n                break;\n            case \"many2one\":\n                value = value ? value.id : false;\n                break;\n            // case \"reference\":\n            //     // TODO\n            //     break;\n        }\n        serverValues[fieldName] = value;\n    }\n    return serverValues;\n}\n\n/**\n * @param {any} field\n * @returns {boolean}\n */\nexport function isRelational(field) {\n    return field && [\"one2many\", \"many2many\", \"many2one\"].includes(field.type);\n}\n\n/**\n * This hook should only be used in a component field because it\n * depends on the record props.\n * The callback will be executed once during setup and each time\n * a record value read in the callback changes.\n * @param {(record) => void} callback\n */\nexport function useRecordObserver(callback) {\n    const component = useComponent();\n    let alive = true;\n    let props = component.props;\n    const fct = () => {\n        const def = new Deferred();\n        let firstCall = true;\n        effect(\n            (record) => {\n                if (firstCall) {\n                    firstCall = false;\n                    return Promise.resolve(callback(record, props))\n                        .then(def.resolve)\n                        .catch(def.reject);\n                } else {\n                    return batched(\n                        (record) => {\n                            if (!alive) {\n                                // effect doesn't clean up when the component is unmounted.\n                                // We must do it manually.\n                                return;\n                            }\n                            return Promise.resolve(callback(record, props))\n                                .then(def.resolve)\n                                .catch(def.reject);\n                        },\n                        () => new Promise((resolve) => window.requestAnimationFrame(resolve))\n                    )(record);\n                }\n            },\n            [props.record]\n        );\n        return def;\n    };\n    onWillDestroy(() => {\n        alive = false;\n    });\n    onWillStart(() => fct());\n    onWillUpdateProps((nextProps) => {\n        const currentRecordId = props.record.id;\n        props = nextProps;\n        if (props.record.id !== currentRecordId) {\n            return fct();\n        }\n    });\n}\n\n/**\n * Resequence records based on provided parameters.\n *\n * @param {Object} params\n * @param {Array} params.records - The list of records to resequence.\n * @param {string} params.resModel - The model to be used for resequencing.\n * @param {Object} params.orm\n * @param {string} params.fieldName - The field used to handle the sequence.\n * @param {number} params.movedId - The id of the record being moved.\n * @param {number} [params.targetId] - The id of the target position, the record will be resequenced\n *                                     after the target. If undefined, the record will be resequenced\n *                                     as the first record.\n * @param {Boolean} [params.asc] - Resequence in ascending or descending order\n * @param {Function} [params.getSequence] - Function to get the sequence of a record.\n * @param {Function} [params.getResId] - Function to get the resID of the record.\n * @param {Object} [params.context]\n * @returns {Promise<any>} - The list of the resequenced fieldName\n */\nexport async function resequence({\n    records,\n    resModel,\n    orm,\n    fieldName,\n    movedId,\n    targetId,\n    asc = true,\n    getSequence = (record) => record[fieldName],\n    getResId = (record) => record.id,\n    context,\n}) {\n    // Find indices\n    const fromIndex = records.findIndex((d) => d.id === movedId);\n    let toIndex = 0;\n    if (targetId !== null) {\n        const targetIndex = records.findIndex((d) => d.id === targetId);\n        toIndex = fromIndex > targetIndex ? targetIndex + 1 : targetIndex;\n    }\n\n    // Determine which records/groups need to be modified\n    const firstIndex = Math.min(fromIndex, toIndex);\n    const lastIndex = Math.max(fromIndex, toIndex) + 1;\n    let reorderAll = records.some((record) => getSequence(record) === undefined);\n    if (!reorderAll) {\n        let lastSequence = (asc ? -1 : 1) * Infinity;\n        for (let index = 0; index < records.length; index++) {\n            const sequence = getSequence(records[index]);\n            if ((asc && lastSequence >= sequence) || (!asc && lastSequence <= sequence)) {\n                reorderAll = true;\n                break;\n            }\n            lastSequence = sequence;\n        }\n    }\n\n    // Save the original list in case of error\n    const originalOrder = [...records];\n    // Perform the resequence in the list of records/groups\n    const record = records[fromIndex];\n    if (fromIndex !== toIndex) {\n        records.splice(fromIndex, 1);\n        records.splice(toIndex, 0, record);\n    }\n\n    // Creates the list of records/groups to modify\n    let toReorder = records;\n    if (!reorderAll) {\n        toReorder = toReorder.slice(firstIndex, lastIndex).filter((r) => r.id !== movedId);\n        if (fromIndex < toIndex) {\n            toReorder.push(record);\n        } else {\n            toReorder.unshift(record);\n        }\n    }\n    if (!asc) {\n        toReorder.reverse();\n    }\n\n    const resIds = toReorder.map((d) => getResId(d)).filter((id) => id && !isNaN(id));\n    const sequences = toReorder.map(getSequence);\n    const offset = sequences.length && Math.min(...sequences);\n\n    // Try to write new sequences on the affected records/groups\n    const params = {\n        model: resModel,\n        ids: resIds,\n        context: context,\n        field: fieldName,\n    };\n    if (offset) {\n        params.offset = offset;\n    }\n    try {\n        const wasResequenced = await rpc(\"/web/dataset/resequence\", params);\n        if (!wasResequenced) {\n            return;\n        }\n    } catch (error) {\n        // If the server fails to resequence, rollback the original list\n        records.splice(0, records.length, ...originalOrder);\n        throw error;\n    }\n\n    // Read the actual values set by the server and update the records/groups\n    const kwargs = { context };\n    return orm.read(resModel, resIds, [fieldName], kwargs);\n}\n", "import {\n    deserializeDate,\n    deserializeDateTime,\n    parseDate,\n    serializeDate,\n    serializeDateTime,\n} from \"@web/core/l10n/dates\";\nimport { ORM } from \"@web/core/orm_service\";\nimport { registry } from \"@web/core/registry\";\nimport { cartesian, sortBy as arraySortBy } from \"@web/core/utils/arrays\";\nimport { parseServerValue } from \"./relational_model/utils\";\n\nclass UnimplementedRouteError extends Error {}\n\nlet searchReadNumber = 0;\n\n/**\n * Helper function returning the value from a list of sample strings\n * corresponding to the given ID.\n * @param {number} id\n * @param {string[]} sampleTexts\n * @returns {string}\n */\nfunction getSampleFromId(id, sampleTexts) {\n    return sampleTexts[(id - 1) % sampleTexts.length];\n}\n\nfunction serializeGroupDateValue(range, field) {\n    if (!range) {\n        return false;\n    }\n    let dateValue = parseServerValue(field, range.to);\n    dateValue = dateValue.minus({\n        [field.type === \"date\" ? \"day\" : \"second\"]: 1,\n    });\n    return field.type === \"date\" ? serializeDate(dateValue) : serializeDateTime(dateValue);\n}\n\n/**\n * Helper function returning a regular expression specifically matching\n * a given 'term' in a fieldName. For example `fieldNameRegex('abc')`:\n * will match:\n * - \"abc\"\n * - \"field_abc__def\"\n * will not match:\n * - \"aabc\"\n * - \"abcd_ef\"\n * @param {...string} term\n * @returns {RegExp}\n */\nfunction fieldNameRegex(...terms) {\n    return new RegExp(`\\\\b((\\\\w+)?_)?(${terms.join(\"|\")})(_(\\\\w+)?)?\\\\b`);\n}\n\nconst MEASURE_SPEC_REGEX = /(?<measure>\\w+):(?<aggregateFunction>\\w+)(\\((?<fieldName>\\w+)\\))?/;\nconst DESCRIPTION_REGEX = fieldNameRegex(\"description\", \"label\", \"title\", \"subject\", \"message\");\nconst EMAIL_REGEX = fieldNameRegex(\"email\");\nconst PHONE_REGEX = fieldNameRegex(\"phone\");\nconst URL_REGEX = fieldNameRegex(\"url\");\n\n/**\n * Sample server class\n *\n * Represents a static instance of the server used when a RPC call sends\n * empty values/groups while the attribute 'sample' is set to true on the\n * view.\n *\n * This server will generate fake data and send them in the adequate format\n * according to the route/method used in the RPC.\n */\nexport class SampleServer {\n    /**\n     * @param {string} modelName\n     * @param {Object} fields\n     */\n    constructor(modelName, fields) {\n        this.mainModel = modelName;\n        this.data = {};\n        this.data[modelName] = {\n            fields,\n            records: [],\n        };\n        // Generate relational fields' co models\n        for (const fieldName in fields) {\n            const field = fields[fieldName];\n            if ([\"many2one\", \"one2many\", \"many2many\"].includes(field.type)) {\n                this.data[field.relation] = this.data[field.relation] || {\n                    fields: {\n                        display_name: { type: \"char\" },\n                        id: { type: \"integer\" },\n                        color: { type: \"integer\" },\n                    },\n                    records: [],\n                };\n            }\n        }\n        // On some models, empty grouped Kanban or List view still contain\n        // real (empty) groups. In this case, we re-use the result of the\n        // web_read_group rpc to tweak sample data s.t. those real groups\n        // contain sample records.\n        this.existingGroups = null;\n        // Sample records generation is only done if necessary, so we delay\n        // it to the first \"mockRPC\" call. These flags allow us to know if\n        // the records have been generated or not.\n        this.populated = false;\n        this.existingGroupsPopulated = false;\n    }\n\n    //---------------------------------------------------------------------\n    // Public\n    //---------------------------------------------------------------------\n\n    /**\n     * This is the main entry point of the SampleServer. Mocks a request to\n     * the server with sample data.\n     * @param {Object} params\n     * @returns {any} the result obtained with the sample data\n     * @throws {Error} If called on a route/method we do not handle\n     */\n    mockRpc(params) {\n        if (!(params.model in this.data)) {\n            throw new Error(`SampleServer: unknown model ${params.model}`);\n        }\n        this._populateModels();\n        switch (params.method || params.route) {\n            case \"web_search_read\":\n                return this._mockWebSearchReadUnity(params);\n            case \"web_read_group\":\n                return this._mockWebReadGroup(params);\n            case \"read_group\":\n                return this._mockReadGroup(params);\n            case \"read_progress_bar\":\n                return this._mockReadProgressBar(params);\n            case \"read\":\n                return this._mockRead(params);\n        }\n        // this rpc can't be mocked by the SampleServer itself, so check if there is an handler\n        // in the registry: either specific for this model (with key 'model/method'), or\n        // global (with key 'method')\n        const method = params.method || params.route;\n        // This allows to register mock version of methods or routes,\n        // for all models:\n        // registry.category(\"sample_server\").add('some_route', () => \"abcd\");\n        // for a specific model (e.g. 'res.partner'):\n        // registry.category(\"sample_server\").add('res.partner/some_method', () => 23);\n        const mockFunction =\n            registry.category(\"sample_server\").get(`${params.model}/${method}`, null) ||\n            registry.category(\"sample_server\").get(method, null);\n        if (mockFunction) {\n            return mockFunction.call(this, params);\n        }\n        console.log(`SampleServer: unimplemented route \"${params.method || params.route}\"`);\n        throw new SampleServer.UnimplementedRouteError();\n    }\n\n    setExistingGroups(groups) {\n        this.existingGroups = groups;\n    }\n\n    //---------------------------------------------------------------------\n    // Private\n    //---------------------------------------------------------------------\n\n    /**\n     * @param {Object[]} measures, each measure has the form { fieldName, type }\n     * @param {Object[]} records\n     * @returns {Object}\n     */\n    _aggregateFields(measures, records) {\n        const values = {};\n        for (const { fieldName, type, aggregateFunction } of measures) {\n            if ([\"float\", \"integer\", \"monetary\"].includes(type)) {\n                if (aggregateFunction === \"array_agg\") {\n                    values[fieldName] = (records || []).map((r) => r[fieldName]);\n                } else if (records.length) {\n                    let value = 0;\n                    for (const record of records) {\n                        value += record[fieldName];\n                    }\n                    values[fieldName] = this._sanitizeNumber(value);\n                } else {\n                    values[fieldName] = null;\n                }\n            }\n            if (type === \"many2one\") {\n                const ids = new Set(records.map((r) => r[fieldName]));\n                values.fieldName = ids.size || null;\n            }\n        }\n        return values;\n    }\n\n    /**\n     * @param {any} value\n     * @param {Object} options\n     * @param {string} [options.interval]\n     * @param {string} [options.relation]\n     * @param {string} [options.type]\n     * @returns {any}\n     */\n    _formatValue(value, options) {\n        if (!value) {\n            return false;\n        }\n        const { type, interval, relation } = options;\n        if ([\"date\", \"datetime\"].includes(type)) {\n            const fmt = SampleServer.FORMATS[interval];\n            return parseDate(value).toFormat(fmt);\n        } else if ([\"many2one\", \"many2many\"].includes(type)) {\n            const rec = this.data[relation].records.find(({ id }) => id === value);\n            return [value, rec.display_name];\n        } else {\n            return value;\n        }\n    }\n\n    /**\n     * Generates field values based on heuristics according to field types\n     * and names.\n     *\n     * @private\n     * @param {string} modelName\n     * @param {string} fieldName\n     * @param {number} id the record id\n     * @returns {any} the field value\n     */\n    _generateFieldValue(modelName, fieldName, id) {\n        const field = this.data[modelName].fields[fieldName];\n        switch (field.type) {\n            case \"boolean\":\n                return fieldName === \"active\" ? true : this._getRandomBool();\n            case \"char\":\n            case \"text\":\n                if ([\"display_name\", \"name\"].includes(fieldName)) {\n                    if (SampleServer.PEOPLE_MODELS.includes(modelName)) {\n                        return getSampleFromId(id, SampleServer.SAMPLE_PEOPLE);\n                    } else if (modelName === \"res.country\") {\n                        return getSampleFromId(id, SampleServer.SAMPLE_COUNTRIES);\n                    }\n                }\n                if (fieldName === \"display_name\") {\n                    return getSampleFromId(id, SampleServer.SAMPLE_TEXTS);\n                } else if ([\"name\", \"reference\"].includes(fieldName)) {\n                    return `REF${String(id).padStart(4, \"0\")}`;\n                } else if (DESCRIPTION_REGEX.test(fieldName)) {\n                    return getSampleFromId(id, SampleServer.SAMPLE_TEXTS);\n                } else if (EMAIL_REGEX.test(fieldName)) {\n                    const emailName = getSampleFromId(id, SampleServer.SAMPLE_PEOPLE)\n                        .replace(/ /, \".\")\n                        .toLowerCase();\n                    return `${emailName}@sample.demo`;\n                } else if (PHONE_REGEX.test(fieldName)) {\n                    return `+1 555 754 ${String(id).padStart(4, \"0\")}`;\n                } else if (URL_REGEX.test(fieldName)) {\n                    return `http://sample${id}.com`;\n                }\n                return false;\n            case \"date\":\n            case \"datetime\": {\n                const datetime = this._getRandomDate();\n                return field.type === \"date\"\n                    ? serializeDate(datetime)\n                    : serializeDateTime(datetime);\n            }\n            case \"float\":\n                return this._getRandomFloat(SampleServer.MAX_FLOAT);\n            case \"integer\": {\n                let max = SampleServer.MAX_INTEGER;\n                if (fieldName.includes(\"color\")) {\n                    max = this._getRandomBool() ? SampleServer.MAX_COLOR_INT : 0;\n                }\n                return this._getRandomInt(max);\n            }\n            case \"monetary\":\n                return this._getRandomInt(SampleServer.MAX_MONETARY);\n            case \"many2one\":\n                if (field.relation === \"res.currency\") {\n                    /** @todo return session.company_currency_id */\n                    return 1;\n                }\n                if (field.relation === \"ir.attachment\") {\n                    return false;\n                }\n                return this._getRandomSubRecordId();\n            case \"one2many\":\n            case \"many2many\": {\n                const ids = [this._getRandomSubRecordId(), this._getRandomSubRecordId()];\n                return [...new Set(ids)];\n            }\n            case \"selection\": {\n                return this._getRandomSelectionValue(modelName, field);\n            }\n            default:\n                return false;\n        }\n    }\n\n    /**\n     * @private\n     * @param {any[]} array\n     * @returns {any}\n     */\n    _getRandomArrayEl(array) {\n        return array[Math.floor(Math.random() * array.length)];\n    }\n\n    /**\n     * @private\n     * @returns {boolean}\n     */\n    _getRandomBool() {\n        return Math.random() < 0.5;\n    }\n\n    /**\n     * @private\n     * @returns {DateTime}\n     */\n    _getRandomDate() {\n        const delta = Math.floor((Math.random() - Math.random()) * SampleServer.DATE_DELTA);\n        return luxon.DateTime.local().plus({ hours: delta });\n    }\n\n    /**\n     * @private\n     * @param {number} max\n     * @returns {number} float in [O, max[\n     */\n    _getRandomFloat(max) {\n        return this._sanitizeNumber(Math.random() * max);\n    }\n\n    /**\n     * @private\n     * @param {number} max\n     * @returns {number} int in [0, max[\n     */\n    _getRandomInt(max) {\n        return Math.floor(Math.random() * max);\n    }\n\n    /**\n     * @private\n     * @returns {string}\n     */\n    _getRandomSelectionValue(modelName, field) {\n        if (field.selection.length > 0) {\n            return this._getRandomArrayEl(field.selection)[0];\n        }\n        return false;\n    }\n\n    /**\n     * @private\n     * @returns {number} id in [1, SUB_RECORDSET_SIZE]\n     */\n    _getRandomSubRecordId() {\n        return Math.floor(Math.random() * SampleServer.SUB_RECORDSET_SIZE) + 1;\n    }\n\n    /**\n     * Mocks calls to the read method.\n     * @private\n     * @param {Object} params\n     * @param {string} params.model\n     * @param {Array[]} params.args (args[0] is the list of ids, args[1] is\n     *   the list of fields)\n     * @returns {Object[]}\n     */\n    _mockRead(params) {\n        const model = this.data[params.model];\n        const ids = params.args[0];\n        const fieldNames = params.args[1];\n        const records = [];\n        for (const r of model.records) {\n            if (!ids.includes(r.id)) {\n                continue;\n            }\n            const record = { id: r.id };\n            for (const fieldName of fieldNames) {\n                const field = model.fields[fieldName];\n                if (!field) {\n                    record[fieldName] = false; // unknown field\n                } else if (field.type === \"many2one\") {\n                    const relModel = this.data[field.relation];\n                    const relRecord = relModel.records.find((relR) => r[fieldName] === relR.id);\n                    record[fieldName] = relRecord ? [relRecord.id, relRecord.display_name] : false;\n                } else {\n                    record[fieldName] = r[fieldName];\n                }\n            }\n            records.push(record);\n        }\n        return records;\n    }\n\n    /**\n     * Mocks calls to the read_group method.\n     *\n     * @param {Object} params\n     * @param {string} params.model\n     * @param {string[]} [params.fields] defaults to the list of all fields\n     * @param {string[]} params.groupBy\n     * @param {boolean} [params.lazy=true]\n     * @returns {Object[]} Object with keys groups and length\n     */\n    _mockReadGroup(params) {\n        const lazy = \"lazy\" in params ? params.lazy : true;\n        const model = params.model;\n        const fields = this.data[model].fields;\n        const records = this.data[model].records;\n\n        const normalizedGroupBys = [];\n        let groupBy = [];\n        if (params.groupBy.length) {\n            groupBy = lazy ? [params.groupBy[0]] : params.groupBy;\n        }\n        for (const groupBySpec of groupBy) {\n            let [fieldName, interval] = groupBySpec.split(\":\");\n            interval = interval || \"month\";\n            const { type, relation } = fields[fieldName];\n            if (type) {\n                const gb = { fieldName, type, interval, relation, alias: groupBySpec };\n                normalizedGroupBys.push(gb);\n            }\n        }\n\n        const groupsFromRecord = (record) => {\n            const values = [];\n            for (const gb of normalizedGroupBys) {\n                const { fieldName, type } = gb;\n                let fieldVals;\n                if ([\"date\", \"datetime\"].includes(type)) {\n                    fieldVals = [this._formatValue(record[fieldName], gb)];\n                } else if (type === \"many2many\") {\n                    fieldVals = record[fieldName].length ? record[fieldName] : [false];\n                } else {\n                    fieldVals = [record[fieldName]];\n                }\n                values.push(fieldVals.map((val) => ({ [fieldName]: val })));\n            }\n            const cart = cartesian(...values);\n            return cart.map((tuple) => {\n                if (!Array.isArray(tuple)) {\n                    tuple = [tuple];\n                }\n                return Object.assign({}, ...tuple);\n            });\n        };\n\n        const groups = {};\n        for (const record of records) {\n            const recordGroups = groupsFromRecord(record);\n            for (const group of recordGroups) {\n                const groupId = JSON.stringify(group);\n                if (!(groupId in groups)) {\n                    groups[groupId] = [];\n                }\n                groups[groupId].push(record);\n            }\n        }\n\n        const measures = [];\n        for (const measureSpec of params.fields || Object.keys(fields)) {\n            const matches = measureSpec.match(MEASURE_SPEC_REGEX);\n            let { fieldName, aggregateFunction, measure } = (matches && matches.groups) || {};\n            if (!aggregateFunction && fieldName in fields && fields[fieldName].aggregator) {\n                aggregateFunction = fields[fieldName].aggregator;\n                measure = fieldName;\n            }\n            if (!fieldName && !measure) {\n                continue; // this is for _count measure\n            }\n            const fName = fieldName || measure;\n            const { type } = fields[fName];\n            if (\n                !params.groupBy.includes(fName) &&\n                type &&\n                (type !== \"many2one\" || aggregateFunction !== \"count_distinct\")\n            ) {\n                measures.push({ fieldName: fName, type, aggregateFunction });\n            }\n        }\n\n        let result = [];\n        for (const id in groups) {\n            const records = groups[id];\n            const group = { __domain: [] };\n            let countKey = `__count`;\n            if (normalizedGroupBys.length && lazy) {\n                countKey = `${normalizedGroupBys[0].fieldName}_count`;\n            }\n            group[countKey] = records.length;\n            const firstElem = records[0];\n            const parsedId = JSON.parse(id);\n            for (const gb of normalizedGroupBys) {\n                const { alias, fieldName, type } = gb;\n                if (type === \"many2many\") {\n                    group[alias] = this._formatValue(parsedId[fieldName], gb);\n                } else {\n                    group[alias] = this._formatValue(firstElem[fieldName], gb);\n                    if ([\"date\", \"datetime\"].includes(type)) {\n                        group.__range = {};\n                        const val = firstElem[fieldName];\n                        if (val) {\n                            const deserialize =\n                                type === \"date\" ? deserializeDate : deserializeDateTime;\n                            const serialize = type === \"date\" ? serializeDate : serializeDateTime;\n                            const from = deserialize(val).startOf(gb.interval);\n                            const to = SampleServer.INTERVALS[gb.interval](from);\n                            group.__range[alias] = { from: serialize(from), to: serialize(to) };\n                        } else {\n                            group.__range[alias] = false;\n                        }\n                    }\n                }\n            }\n            Object.assign(group, this._aggregateFields(measures, records));\n            result.push(group);\n        }\n        if (normalizedGroupBys.length > 0) {\n            const { alias, interval, type } = normalizedGroupBys[0];\n            result = arraySortBy(result, (group) => {\n                const val = group[alias];\n                if ([\"date\", \"datetime\"].includes(type)) {\n                    return parseDate(val, { format: SampleServer.FORMATS[interval] });\n                }\n                return val;\n            });\n        }\n        return result;\n    }\n\n    /**\n     * Mocks calls to the read_progress_bar method.\n     * @private\n     * @param {Object} params\n     * @param {string} params.model\n     * @param {string} params.group_by\n     * @param {Object} params.progress_bar\n     * @return {Object}\n     */\n    _mockReadProgressBar(params) {\n        const groupBy = params.group_by.split(\":\")[0];\n        const progress_bar = params.progress_bar;\n        const groupByField = this.data[params.model].fields[groupBy];\n        const data = {};\n        for (const record of this.data[params.model].records) {\n            let groupByValue = record[groupBy];\n            if (groupByField.type === \"many2one\") {\n                const relatedRecords = this.data[groupByField.relation].records;\n                const relatedRecord = relatedRecords.find((r) => r.id === groupByValue);\n                groupByValue = relatedRecord.display_name;\n            }\n            // special case for bool values: rpc call response with capitalized strings\n            if (!(groupByValue in data)) {\n                if (groupByValue === true) {\n                    groupByValue = \"True\";\n                } else if (groupByValue === false) {\n                    groupByValue = \"False\";\n                }\n            }\n            if (!(groupByValue in data)) {\n                data[groupByValue] = {};\n                for (const key in progress_bar.colors) {\n                    data[groupByValue][key] = 0;\n                }\n            }\n            const fieldValue = record[progress_bar.field];\n            if (fieldValue in data[groupByValue]) {\n                data[groupByValue][fieldValue]++;\n            }\n        }\n        return data;\n    }\n\n    _mockWebSearchReadUnity(params) {\n        const fields = Object.keys(params.specification);\n        let result;\n        if (this.existingGroups) {\n            const groups = this.existingGroups;\n            const group = groups[searchReadNumber++ % groups.length];\n            result = {\n                records: this._mockRead({\n                    model: params.model,\n                    args: [group.__recordIds, fields],\n                }),\n                length: group.__recordIds.length,\n            };\n        } else {\n            const model = this.data[params.model];\n            const rawRecords = model.records.slice(0, SampleServer.SEARCH_READ_LIMIT);\n            const records = this._mockRead({\n                model: params.model,\n                args: [rawRecords.map((r) => r.id), fields],\n            });\n            result = { records, length: records.length };\n        }\n        // populate many2one and x2many values\n        for (const fieldName in params.specification) {\n            const field = this.data[params.model].fields[fieldName];\n            if (field.type === \"many2one\") {\n                for (const record of result.records) {\n                    record[fieldName] = record[fieldName]\n                        ? {\n                              id: record[fieldName][0],\n                              display_name: record[fieldName][1],\n                          }\n                        : false;\n                }\n            }\n            if (field.type === \"one2many\" || field.type === \"many2many\") {\n                const relFields = Object.keys(params.specification[fieldName].fields || {});\n                if (relFields.length) {\n                    const relIds = result.records.map((r) => r[fieldName]).flat();\n                    const relRecords = {};\n                    const _relRecords = this._mockRead({\n                        model: field.relation,\n                        args: [relIds, relFields],\n                    });\n                    for (const relRecord of _relRecords) {\n                        relRecords[relRecord.id] = relRecord;\n                    }\n                    for (const record of result.records) {\n                        record[fieldName] = record[fieldName].map((resId) => relRecords[resId]);\n                    }\n                }\n            }\n        }\n        return result;\n    }\n\n    /**\n     * Mocks calls to the web_read_group method to return groups populated\n     * with sample records. Only handles the case where the real call to\n     * web_read_group returned groups, but none of these groups contain\n     * records. In this case, we keep the real groups, and populate them\n     * with sample records.\n     * @private\n     * @param {Object} params\n     * @param {Object} [result] the result of a real call to web_read_group\n     * @returns {{ groups: Object[], length: number }}\n     */\n    _mockWebReadGroup(params) {\n        let groups;\n        if (this.existingGroups) {\n            this._tweakExistingGroups(params);\n            groups = this.existingGroups;\n        } else {\n            groups = this._mockReadGroup(params);\n        }\n        return {\n            groups,\n            length: groups.length,\n        };\n    }\n\n    /**\n     * Updates the sample data such that the existing groups (in database)\n     * also exists in the sample, and such that there are sample records in\n     * those groups.\n     * @private\n     * @param {Object[]} groups empty groups returned by the server\n     * @param {Object} params\n     * @param {string} params.model\n     * @param {string[]} params.groupBy\n     */\n    _populateExistingGroups(params) {\n        const groups = this.existingGroups;\n        const groupBy = params.groupBy[0].split(\":\")[0];\n        const groupByField = this.data[params.model].fields[groupBy];\n        const groupedByM2O = groupByField.type === \"many2one\";\n        if (groupedByM2O) {\n            // re-populate co model with relevant records\n            this.data[groupByField.relation].records = groups.map((g) => {\n                return { id: g[groupBy][0], display_name: g[groupBy][1] };\n            });\n        }\n        for (const r of this.data[params.model].records) {\n            const group = getSampleFromId(r.id, groups);\n            if ([\"date\", \"datetime\"].includes(groupByField.type)) {\n                r[groupBy] = serializeGroupDateValue(\n                    group.__range[params.groupBy[0]],\n                    groupByField\n                );\n            } else if (groupByField.type === \"many2one\") {\n                r[groupBy] = group[params.groupBy[0]] ? group[params.groupBy[0]][0] : false;\n            } else {\n                r[groupBy] = group[params.groupBy[0]];\n            }\n        }\n    }\n\n    /**\n     * Generates sample records for the models in this.data. Records will be\n     * generated once, and subsequent calls to this function will be skipped.\n     * @private\n     */\n    _populateModels() {\n        if (!this.populated) {\n            for (const modelName in this.data) {\n                const model = this.data[modelName];\n                const fieldNames = Object.keys(model.fields).filter((f) => f !== \"id\");\n                const size =\n                    modelName === this.mainModel\n                        ? SampleServer.MAIN_RECORDSET_SIZE\n                        : SampleServer.SUB_RECORDSET_SIZE;\n                for (let id = 1; id <= size; id++) {\n                    const record = { id };\n                    for (const fieldName of fieldNames) {\n                        record[fieldName] = this._generateFieldValue(modelName, fieldName, id);\n                    }\n                    model.records.push(record);\n                }\n            }\n            this.populated = true;\n        }\n    }\n\n    /**\n     * Rounds the given number value according to the configured precision.\n     * @private\n     * @param {number} value\n     * @returns {number}\n     */\n    _sanitizeNumber(value) {\n        return parseFloat(value.toFixed(SampleServer.FLOAT_PRECISION));\n    }\n\n    /**\n     * A real (web_)read_group call has been done, and it has returned groups,\n     * but they are all empty. This function updates the sample data such\n     * that those group values exist and those groups contain sample records.\n     * @private\n     * @param {Object[]} groups empty groups returned by the server\n     * @param {Object} params\n     * @param {string} params.model\n     * @param {string[]} params.fields\n     * @param {string[]} params.groupBy\n     * @returns {Object[]} groups with count and aggregate values updated\n     *\n     * TODO: rename\n     */\n    _tweakExistingGroups(params) {\n        const groups = this.existingGroups;\n        this._populateExistingGroups(params);\n\n        // update count and aggregates for each group\n        const fullGroupBy = params.groupBy[0];\n        const groupBy = fullGroupBy.split(\":\")[0];\n        const groupByField = this.data[params.model].fields[groupBy];\n        const records = this.data[params.model].records;\n        const fields = params.fields.map((aggregate_spec) => aggregate_spec.split(\":\")[0])\n        for (const g of groups) {\n            const recordsInGroup = records.filter((r) => {\n                if ([\"date\", \"datetime\"].includes(groupByField.type)) {\n                    return (\n                        r[groupBy] === serializeGroupDateValue(g.__range[fullGroupBy], groupByField)\n                    );\n                } else if (groupByField.type === \"many2one\") {\n                    return (!r[groupBy] && !g[fullGroupBy]) || r[groupBy] === g[fullGroupBy][0];\n                }\n                return r[groupBy] === g[fullGroupBy];\n            });\n            for (const field of fields) {\n                const fieldType = this.data[params.model].fields[field].type;\n                if ([\"integer, float\", \"monetary\"].includes(fieldType)) {\n                    g[field] = recordsInGroup.reduce((acc, r) => acc + r[field], 0);\n                }\n            }\n            g[`${groupBy}_count`] = recordsInGroup.length;\n            g.__recordIds = recordsInGroup.map((r) => r.id);\n        }\n    }\n}\n\nSampleServer.FORMATS = {\n    day: \"yyyy-MM-dd\",\n    week: \"'W'WW kkkk\",\n    month: \"MMMM yyyy\",\n    quarter: \"'Q'q yyyy\",\n    year: \"y\",\n};\nSampleServer.INTERVALS = {\n    day: (dt) => dt.plus({ days: 1 }),\n    week: (dt) => dt.plus({ weeks: 1 }),\n    month: (dt) => dt.plus({ months: 1 }),\n    quarter: (dt) => dt.plus({ months: 3 }),\n    year: (dt) => dt.plus({ years: 1 }),\n};\nSampleServer.DISPLAY_FORMATS = Object.assign({}, SampleServer.FORMATS, { day: \"dd MMM yyyy\" });\n\nSampleServer.MAIN_RECORDSET_SIZE = 16;\nSampleServer.SUB_RECORDSET_SIZE = 5;\nSampleServer.SEARCH_READ_LIMIT = 10;\n\nSampleServer.MAX_FLOAT = 100;\nSampleServer.MAX_INTEGER = 50;\nSampleServer.MAX_COLOR_INT = 7;\nSampleServer.MAX_MONETARY = 100000;\nSampleServer.DATE_DELTA = 24 * 60; // in hours -> 60 days\nSampleServer.FLOAT_PRECISION = 2;\n\nSampleServer.SAMPLE_COUNTRIES = [\"Belgium\", \"France\", \"Portugal\", \"Singapore\", \"Australia\"];\nSampleServer.SAMPLE_PEOPLE = [\n    \"John Miller\",\n    \"Henry Campbell\",\n    \"Carrie Helle\",\n    \"Wendi Baltz\",\n    \"Thomas Passot\",\n];\nSampleServer.SAMPLE_TEXTS = [\n    \"Laoreet id\",\n    \"Volutpat blandit\",\n    \"Integer vitae\",\n    \"Viverra nam\",\n    \"In massa\",\n];\nSampleServer.PEOPLE_MODELS = [\n    \"res.users\",\n    \"res.partner\",\n    \"hr.employee\",\n    \"mail.followers\",\n    \"mailing.contact\",\n];\n\nSampleServer.UnimplementedRouteError = UnimplementedRouteError;\n\nexport function buildSampleORM(resModel, fields, user) {\n    const sampleServer = new SampleServer(resModel, fields);\n    const fakeRPC = async (_, params) => {\n        const { args, kwargs, method, model } = params;\n        const { groupby: groupBy } = kwargs;\n        return sampleServer.mockRpc({ method, model, args, ...kwargs, groupBy });\n    };\n    const sampleORM = new ORM(user);\n    sampleORM.rpc = fakeRPC;\n    sampleORM.isSample = true;\n    sampleORM.setGroups = (groups) => sampleServer.setExistingGroups(groups);\n    return sampleORM;\n}\n", "import { onMounted, useComponent, useEffect, useExternalListener } from \"@odoo/owl\";\n\nexport const scrollSymbol = Symbol(\"scroll\");\n\nexport class CallbackRecorder {\n    constructor() {\n        this.setup();\n    }\n    setup() {\n        this._callbacks = [];\n    }\n    /**\n     * @returns {Function[]}\n     */\n    get callbacks() {\n        return this._callbacks.map(({ callback }) => callback);\n    }\n    /**\n     * @param {any} owner\n     * @param {Function} callback\n     */\n    add(owner, callback) {\n        if (!callback) {\n            throw new Error(\"Missing callback\");\n        }\n        this._callbacks.push({ owner, callback });\n    }\n    /**\n     * @param {any} owner\n     */\n    remove(owner) {\n        this._callbacks = this._callbacks.filter((s) => s.owner !== owner);\n    }\n}\n\n/**\n * @param {CallbackRecorder} callbackRecorder\n * @param {Function} callback\n */\nexport function useCallbackRecorder(callbackRecorder, callback) {\n    const component = useComponent();\n    useEffect(\n        () => {\n            callbackRecorder.add(component, callback);\n            return () => callbackRecorder.remove(component);\n        },\n        () => []\n    );\n}\n\n/**\n */\nexport function useSetupAction(params = {}) {\n    const component = useComponent();\n    const {\n        __beforeLeave__,\n        __getGlobalState__,\n        __getLocalState__,\n        __getContext__,\n        __getOrderBy__,\n    } = component.env;\n\n    const {\n        beforeVisibilityChange,\n        beforeUnload,\n        beforeLeave,\n        getGlobalState,\n        getLocalState,\n        rootRef,\n    } = params;\n\n    if (beforeVisibilityChange) {\n        useExternalListener(document, \"visibilitychange\", beforeVisibilityChange);\n    }\n\n    if (beforeUnload) {\n        useExternalListener(window, \"beforeunload\", beforeUnload);\n    }\n    if (__beforeLeave__ && beforeLeave) {\n        useCallbackRecorder(__beforeLeave__, beforeLeave);\n    }\n    if (__getGlobalState__ && (getGlobalState || rootRef)) {\n        useCallbackRecorder(__getGlobalState__, () => {\n            const state = {};\n            if (getGlobalState) {\n                Object.assign(state, getGlobalState());\n            }\n            return state;\n        });\n    }\n    if (__getLocalState__ && (getLocalState || rootRef)) {\n        useCallbackRecorder(__getLocalState__, () => {\n            const state = {};\n            if (getLocalState) {\n                Object.assign(state, getLocalState());\n            }\n            if (rootRef) {\n                if (component.env.isSmall) {\n                    state[scrollSymbol] = {\n                        root: { left: rootRef.el.scrollLeft, top: rootRef.el.scrollTop },\n                    };\n                } else {\n                    const contentEl =\n                        rootRef.el.querySelector(\n                            \".o_component_with_search_panel > .o_renderer_with_searchpanel,\" +\n                                \".o_component_with_search_panel > .o_renderer\"\n                        ) || rootRef.el.querySelector(\".o_content\");\n                    if (contentEl) {\n                        state[scrollSymbol] = {\n                            content: { left: contentEl.scrollLeft, top: contentEl.scrollTop },\n                        };\n                    }\n                }\n            }\n            return state;\n        });\n\n        if (rootRef) {\n            onMounted(() => {\n                const { state } = component.props;\n                const scrolling = state && state[scrollSymbol];\n                if (scrolling) {\n                    if (component.env.isSmall) {\n                        rootRef.el.scrollTop = (scrolling.root && scrolling.root.top) || 0;\n                        rootRef.el.scrollLeft = (scrolling.root && scrolling.root.left) || 0;\n                    } else if (scrolling.content) {\n                        const contentEl =\n                            rootRef.el.querySelector(\n                                \".o_component_with_search_panel > .o_renderer_with_searchpanel,\" +\n                                    \".o_component_with_search_panel > .o_renderer\"\n                            ) || rootRef.el.querySelector(\".o_content\");\n                        if (contentEl) {\n                            contentEl.scrollTop = scrolling.content.top || 0;\n                            contentEl.scrollLeft = scrolling.content.left || 0;\n                        }\n                    }\n                }\n            });\n        }\n    }\n    if (__getContext__ && params.getContext) {\n        useCallbackRecorder(__getContext__, params.getContext);\n    }\n    if (__getOrderBy__ && params.getOrderBy) {\n        useCallbackRecorder(__getOrderBy__, params.getOrderBy);\n    }\n}\n", "import { browser } from \"@web/core/browser/browser\";\nimport { makeContext } from \"@web/core/context\";\nimport { session } from \"@web/session\";\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\";\n\nimport { Component, onWillStart, onWillUpdateProps, useState } from \"@odoo/owl\";\n\nexport const STATIC_ACTIONS_GROUP_NUMBER = 1;\nexport const ACTIONS_GROUP_NUMBER = 100;\n\n/**\n * Action menus (or Action/Print bar, previously called 'Sidebar')\n *\n * The side bar is the group of dropdown menus located on the left side of the\n * control panel. Its role is to display a list of items depending on the view\n * type and selected records and to execute a set of actions on active records.\n * It is made out of 2 dropdown: Print and Action.\n *\n * @extends Component\n */\nexport class ActionMenus extends Component {\n    static template = \"web.ActionMenus\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n    };\n    static props = {\n        getActiveIds: Function,\n        context: Object,\n        resModel: String,\n        printDropdownTitle: { type: String, optional: true },\n        domain: { type: Array, optional: true },\n        isDomainSelected: { type: Boolean, optional: true },\n        items: {\n            type: Object,\n            shape: {\n                action: { type: Array, optional: true },\n                print: { type: Array, optional: true },\n            },\n        },\n        onActionExecuted: { type: Function, optional: true },\n        shouldExecuteAction: { type: Function, optional: true },\n        loadExtraPrintItems: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        printDropdownTitle: _t(\"Print\"),\n        onActionExecuted: () => {},\n        shouldExecuteAction: () => true,\n        loadExtraPrintItems: () => [],\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.actionService = useService(\"action\");\n        this.state = useState({ printItems: []})\n        onWillStart(async () => {\n            this.actionItems = await this.getActionItems(this.props);\n        });\n        onWillUpdateProps(async (nextProps) => {\n            this.actionItems = await this.getActionItems(nextProps);\n        });\n    }\n\n    //---------------------------------------------------------------------\n    // Private\n    //---------------------------------------------------------------------\n\n    async getActionItems(props) {\n        return (props.items.action || []).map((action) => {\n            if (action.callback) {\n                return Object.assign(\n                    { key: `action-${action.description}`, groupNumber: ACTIONS_GROUP_NUMBER },\n                    action\n                );\n            } else {\n                return {\n                    action,\n                    description: action.name,\n                    key: action.id,\n                    groupNumber: action.groupNumber || ACTIONS_GROUP_NUMBER,\n                };\n            }\n        });\n    }\n\n    //---------------------------------------------------------------------\n    // Handlers\n    //---------------------------------------------------------------------\n\n    async executeAction(action) {\n        let activeIds = this.props.getActiveIds();\n        if (this.props.isDomainSelected) {\n            activeIds = await this.orm.search(this.props.resModel, this.props.domain, {\n                limit: session.active_ids_limit,\n                context: this.props.context,\n            });\n        }\n        const activeIdsContext = {\n            active_id: activeIds[0],\n            active_ids: activeIds,\n            active_model: this.props.resModel,\n        };\n        if (this.props.domain) {\n            // keep active_domain in context for backward compatibility\n            // reasons, and to allow actions to bypass the active_ids_limit\n            activeIdsContext.active_domain = this.props.domain;\n        }\n        const context = makeContext([this.props.context, activeIdsContext]);\n        return this.actionService.doAction(action.id, {\n            additionalContext: context,\n            onClose: this.props.onActionExecuted,\n        });\n    }\n\n    /**\n     * Handler used to determine which way must be used to execute a selected\n     * action: it will be either:\n     * - a callback (function given by the view controller);\n     * - an action ID (string);\n     * - an URL (string).\n     * @private\n     * @param {Object} item\n     */\n    async onItemSelected(item) {\n        if (!(await this.props.shouldExecuteAction(item))) {\n            return;\n        }\n        if (item.callback) {\n            item.callback([item]);\n        } else if (item.action) {\n            this.executeAction(item.action);\n        } else if (item.url) {\n            // Event has been prevented at its source: we need to redirect manually.\n            browser.location = item.url;\n        }\n    }\n\n    async loadAvailablePrintItems() {\n        const printActions = this.props.items.print || [];\n        const actionWithDomainIds = [];\n        const validActionIds = [];\n        for (const action of printActions) {\n            \"domain\" in action\n                ? actionWithDomainIds.push(action.id)\n                : validActionIds.push(action.id);\n        }\n        if (actionWithDomainIds.length) {\n            const validActionsWithDomainIds = await this.orm.call(\n                \"ir.actions.report\",\n                \"get_valid_action_reports\",\n                [actionWithDomainIds, this.props.resModel, this.props.getActiveIds()]\n            );\n            validActionIds.push(...validActionsWithDomainIds);\n        }\n        return printActions\n            .filter((action) => validActionIds.includes(action.id))\n            .map((action) => ({\n                action,\n                class: \"o_menu_item\",\n                description: action.name,\n                key: action.id,\n            }));\n    }\n\n    async loadPrintItems() {\n        if (!this.props.items.print?.length) {\n            return;\n        }\n        const [items, extraItems] = await Promise.all([\n            this.loadAvailablePrintItems(),\n            this.props.loadExtraPrintItems(),\n        ]);\n        const allItems = [...extraItems, ...items];\n        if (!allItems.length) {\n            allItems.push({\n                description: _t(\"No report available.\"),\n                class: \"o_menu_item disabled\",\n                key: \"nothing_to_display\",\n            });\n        }\n        this.state.printItems = allItems;\n    }\n}\n", "import { Component } 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\";\n\nexport class Breadcrumbs extends Component {\n    static template = \"web.Breadcrumbs\";\n    static components = { Dropdown, DropdownItem };\n    static props = {\n        breadcrumbs: Array,\n        slots: { type: Object, optional: true },\n    };\n\n    getBreadcrumbTooltip({ isFormView, name }) {\n        if (isFormView) {\n            return _t(\"Back to \u201c%s\u201d form\", name);\n        }\n        return _t(\"Back to \u201c%s\u201d\", name);\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { ActionMenus } from \"@web/search/action_menus/action_menus\";\n\nimport { onWillStart, onWillUpdateProps } from \"@odoo/owl\";\n\nconst cogMenuRegistry = registry.category(\"cogMenu\");\n\n/**\n * Combined Action menus (or Action/Print bar, previously called 'Sidebar')\n *\n * This is a variation of the ActionMenus, combined into a single DropDown.\n *\n * The side bar is the group of dropdown menus located on the left side of the\n * control panel. Its role is to display a list of items depending on the view\n * type and selected records and to execute a set of actions on active records.\n * It is made out of 2 dropdown: Print and Action.\n *\n * @extends ActionMenus\n */\nexport class CogMenu extends ActionMenus {\n    static template = \"web.CogMenu\";\n    static components = {\n        ...ActionMenus.components,\n        Dropdown,\n    };\n    static props = {\n        ...ActionMenus.props,\n        getActiveIds: { type: ActionMenus.props.getActiveIds, optional: true },\n        context: { type: ActionMenus.props.context, optional: true },\n        resModel: { type: ActionMenus.props.resModel, optional: true },\n        items: { ...ActionMenus.props.items, optional: true },\n    };\n    static defaultProps = {\n        ...ActionMenus.defaultProps,\n        items: {},\n    };\n\n    setup() {\n        super.setup();\n        onWillStart(async () => {\n            this.registryItems = await this._registryItems();\n        });\n        onWillUpdateProps(async () => {\n            this.registryItems = await this._registryItems();\n        });\n    }\n\n    get hasItems() {\n        return this.cogItems.length || this.props.items.print?.length;\n    }\n\n    async _registryItems() {\n        const items = [];\n        for (const item of cogMenuRegistry.getAll()) {\n            if (\"isDisplayed\" in item ? await item.isDisplayed(this.env) : true) {\n                items.push({\n                    Component: item.Component,\n                    groupNumber: item.groupNumber,\n                    key: item.Component.name,\n                });\n            }\n        }\n        return items;\n    }\n\n    get cogItems() {\n        return [...this.actionItems, ...this.registryItems].sort((item1, item2) => {\n            const grp = (item1.groupNumber || 0) - (item2.groupNumber || 0);\n            if (grp !== 0) {\n                return grp;\n            }\n            return (item1.sequence || 0) - (item2.sequence || 0);\n        });\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { Pager } from \"@web/core/pager/pager\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useCommand } from \"@web/core/commands/command_hook\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { useSortable } from \"@web/core/utils/sortable_owl\";\nimport { user } from \"@web/core/user\";\nimport { AccordionItem } from \"@web/core/dropdown/accordion_item\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { makeContext } from \"@web/core/context\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { Transition } from \"@web/core/transition\";\nimport { Breadcrumbs } from \"../breadcrumbs/breadcrumbs\";\nimport { SearchBar } from \"../search_bar/search_bar\";\n\nimport { Component, useState, onMounted, useExternalListener, useRef, useEffect } from \"@odoo/owl\";\n\nconst STICKY_CLASS = \"o_mobile_sticky\";\n\n/**\n * @typedef EmbeddedAction\n * @property {number} id\n * @property {[number, string]} parent_action_id\n * @property {string} name\n * @property {number} sequence\n * @property {number} parent_res_id\n * @property {string} parent_res_model\n * @property {[number, string]} action_id\n * @property {string} python_method\n * @property {number} user_id\n * @property {boolean} is_deletable\n * @property {string} default_view_mode\n * @property {string} filter_ids\n * @property {string} domain\n * @property {string} context\n */\n\nexport class ControlPanel extends Component {\n    static template = \"web.ControlPanel\";\n    static components = {\n        Pager,\n        SearchBar,\n        Dropdown,\n        DropdownItem,\n        Breadcrumbs,\n        AccordionItem,\n        CheckBox,\n        Transition,\n    };\n    static props = {\n        display: { type: Object, optional: true },\n        slots: { type: Object, optional: true },\n    };\n\n    setup() {\n        this.actionService = useService(\"action\");\n        this.pagerProps = this.env.config.pagerProps\n            ? useState(this.env.config.pagerProps)\n            : undefined;\n        this.notificationService = useService(\"notification\");\n        this.breadcrumbs = useState(this.env.config.breadcrumbs);\n        this.orm = useService(\"orm\");\n        this.dialogService = useService(\"dialog\");\n\n        this.root = useRef(\"root\");\n        this.newActionNameRef = useRef(\"newActionNameRef\");\n        this.isEmbeddedActionsOrderModifiable = false;\n        this.defaultEmbeddedActions = this.env.config.embeddedActions;\n        if (this.env.config.embeddedActions?.length > 0 && !this.env.config.parentActionId) {\n            const { parent_res_model, parent_action_id } = this.env.config.embeddedActions[0];\n            this.defaultEmbeddedActions = [\n                {\n                    id: false,\n                    name: this.env.config?.actionName,\n                    parent_action_id,\n                    parent_res_model,\n                    action_id: parent_action_id,\n                    user_id: false,\n                    context: {},\n                },\n                ...this.env.config.embeddedActions,\n            ];\n            this.env.config.setEmbeddedActions(this.defaultEmbeddedActions);\n        }\n\n        /**\n         * The visible embedded actions are unique to each user and to each res_id. The visible actions chosen by the\n         * user are stored in the local storage in a key corresponding to a combination of the actionId, the activeId\n         * and the currrent userId. Each key contains a dict. The keys of the latter are the id of the visible embedded\n         * actions.\n         */\n        const parentActionId =\n            this.env.config.parentActionId ||\n            this.env.config.embeddedActions?.[0]?.parent_action_id[0] ||\n            this.env.config.embeddedActions?.[0]?.parent_action_id ||\n            \"\";\n        this.embeddedActionsVisibilityKey = `visibleEmbeddedActions${parentActionId}+${\n            this.env.searchModel?.globalContext.active_id || \"\"\n        }+${user.userId}`;\n\n        this.embeddedVisibilityKey = `visibleEmbedded${parentActionId}+${\n            this.env.searchModel?.globalContext.active_id || \"\"\n        }+${user.userId}`;\n\n        this.embeddedOrderKey = `orderEmbedded${parentActionId}+${\n            this.env.searchModel?.globalContext.active_id || \"\"\n        }+${user.userId}`;\n\n        this.state = useState({\n            showSearchBar: false,\n            showMobileSearch: false,\n            showViewSwitcher: false,\n            embeddedInfos: {\n                showEmbedded:\n                    this.env.config.embeddedActions?.length > 0 &&\n                    ((!!this.env.config.parentActionId &&\n                        !!JSON.parse(browser.localStorage.getItem(\"showEmbeddedActions\"))) ||\n                        !!JSON.parse(browser.localStorage.getItem(this.embeddedVisibilityKey))),\n                embeddedActions: this.defaultEmbeddedActions || [],\n                newActionIsShared: false,\n                newActionName: this.newActionNameGetter,\n                visibleEmbeddedActions:\n                    (this.env.config.embeddedActions?.length > 0 &&\n                        JSON.parse(\n                            browser.localStorage.getItem(this.embeddedActionsVisibilityKey)\n                        )) ||\n                    {},\n                currentEmbeddedAction: this.currentEmbeddedAction,\n            },\n        });\n\n        this.onScrollThrottledBound = this.onScrollThrottled.bind(this);\n\n        const { viewSwitcherEntries, viewType } = this.env.config;\n        for (const view of viewSwitcherEntries || []) {\n            useCommand(_t(\"Show %s view\", view.name), () => this.switchView(view.type), {\n                category: \"view_switcher\",\n                isAvailable: () => view.type !== viewType,\n            });\n        }\n\n        if (viewSwitcherEntries?.length > 1) {\n            useHotkey(\n                \"alt+shift+v\",\n                () => {\n                    this.cycleThroughViews();\n                },\n                {\n                    bypassEditableProtection: true,\n                    withOverlay: () => this.root.el.querySelector(\"nav.o_cp_switch_buttons\"),\n                }\n            );\n        }\n\n        useExternalListener(window, \"click\", this.onWindowClick);\n        useEffect(() => {\n            if (\n                !this.env.isSmall ||\n                (\"adaptToScroll\" in this.display && !this.display.adaptToScroll)\n            ) {\n                return;\n            }\n            const scrollingEl = this.getScrollingElement();\n            scrollingEl.addEventListener(\"scroll\", this.onScrollThrottledBound);\n            this.root.el.style.top = \"0px\";\n            return () => {\n                scrollingEl.removeEventListener(\"scroll\", this.onScrollThrottledBound);\n            };\n        });\n\n        onMounted(async () => {\n            if (this.state.embeddedInfos.embeddedActions?.length > 0) {\n                // If there is no visible embedded actions, the current action (if it exists) is put by default\n                const embeddedActionKey =\n                    this.state.embeddedInfos.currentEmbeddedAction?.id || false;\n                if (\n                    !Object.keys(this.state.embeddedInfos.visibleEmbeddedActions).includes(\n                        embeddedActionKey.toString()\n                    )\n                ) {\n                    this._setVisibility(embeddedActionKey);\n                }\n                const embeddedOrderLocalStorageKey = browser.localStorage.getItem(\n                    this.embeddedOrderKey\n                );\n                if (embeddedOrderLocalStorageKey) {\n                    this._sortEmbeddedActions(JSON.parse(embeddedOrderLocalStorageKey));\n                }\n            }\n            if (\n                !this.env.isSmall ||\n                (\"adaptToScroll\" in this.display && !this.display.adaptToScroll)\n            ) {\n                return;\n            }\n            this.oldScrollTop = 0;\n            this.lastScrollTop = 0;\n            this.initialScrollTop = this.getScrollingElement().scrollTop;\n        });\n\n        this.mainButtons = useRef(\"mainButtons\");\n\n        useEffect(() => {\n            // on small screen, clean-up the dropdown elements\n            const dropdownButtons = this.mainButtons.el.querySelectorAll(\n                \".o_control_panel_collapsed_create.dropdown-menu button\"\n            );\n            if (!dropdownButtons.length) {\n                this.mainButtons.el\n                    .querySelectorAll(\n                        \".o_control_panel_collapsed_create.dropdown-menu, .o_control_panel_collapsed_create.dropdown-toggle\"\n                    )\n                    .forEach((el) => el.classList.add(\"d-none\"));\n                this.mainButtons.el\n                    .querySelectorAll(\".o_control_panel_collapsed_create.btn-group\")\n                    .forEach((el) => el.classList.remove(\"btn-group\"));\n                return;\n            }\n            for (const button of dropdownButtons) {\n                for (const cl of Array.from(button.classList)) {\n                    button.classList.toggle(cl, !cl.startsWith(\"btn-\"));\n                }\n                button.classList.add(\"dropdown-item\", \"btn\", \"btn-link\");\n            }\n        });\n\n        useSortable({\n            enable: true,\n            ref: this.root,\n            elements: \".o_draggable\",\n            cursor: \"move\",\n            delay: 200,\n            tolerance: 10,\n            onWillStartDrag: (params) => this._sortEmbeddedActionStart(params),\n            onDrop: (params) => this._sortEmbeddedActionDrop(params),\n        });\n    }\n\n    getDropdownClass(action) {\n        return (!this.env.isSmall && this._checkValueLocalStorage(action)) ||\n            (this.env.isSmall && this.state.embeddedInfos.currentEmbeddedAction?.id === action.id)\n            ? \"selected\"\n            : \"\";\n    }\n\n    getScrollingElement() {\n        return this.root.el.parentElement;\n    }\n\n    /**\n     * @returns {EmbeddedAction}\n     */\n    get currentEmbeddedAction() {\n        if (!this.env.config) {\n            return {};\n        }\n        const { currentEmbeddedActionId } = this.env.config;\n        return (\n            this.defaultEmbeddedActions?.find(({ id }) => id === currentEmbeddedActionId) ||\n            this.defaultEmbeddedActions?.[0]\n        );\n    }\n\n    get newActionNameGetter() {\n        if (this.currentEmbeddedAction?.name) {\n            return _t(\"Custom %s\", this.currentEmbeddedAction.name);\n        } else {\n            return _t(\"Custom Embedded Action\");\n        }\n    }\n\n    /**\n     * Reset mobile search state\n     */\n    resetSearchState() {\n        Object.assign(this.state, {\n            showSearchBar: false,\n            showMobileSearch: false,\n            showViewSwitcher: false,\n        });\n    }\n\n    /**\n     * @returns {Object}\n     */\n    get display() {\n        return {\n            layoutActions: true,\n            ...this.props.display,\n        };\n    }\n\n    onClickShowEmbedded() {\n        if (this.state.embeddedInfos.showEmbedded) {\n            browser.localStorage.removeItem(this.embeddedVisibilityKey);\n        } else {\n            browser.localStorage.setItem(this.embeddedVisibilityKey, true);\n        }\n        this.state.embeddedInfos.showEmbedded = !this.state.embeddedInfos.showEmbedded;\n        browser.localStorage.setItem(\"showEmbeddedActions\", this.state.embeddedInfos.showEmbedded);\n    }\n\n    /**\n     * Show or hide the control panel on the top screen.\n     * The function is throttled to avoid refreshing the scroll position more\n     * often than necessary.\n     */\n    onScrollThrottled() {\n        if (this.isScrolling) {\n            return;\n        }\n        this.isScrolling = true;\n        browser.requestAnimationFrame(() => (this.isScrolling = false));\n\n        const scrollTop = this.getScrollingElement().scrollTop;\n        const delta = Math.round(scrollTop - this.oldScrollTop);\n\n        if (scrollTop > this.initialScrollTop) {\n            // Beneath initial position => sticky display\n            this.root.el.classList.add(STICKY_CLASS);\n            if (delta < 0) {\n                // Going up\n                this.lastScrollTop = Math.min(0, this.lastScrollTop - delta);\n            } else {\n                // Going down | not moving\n                this.lastScrollTop = Math.max(\n                    -this.root.el.offsetHeight,\n                    -this.root.el.offsetTop - delta\n                );\n            }\n            this.root.el.style.top = `${this.lastScrollTop}px`;\n        } else {\n            // Above initial position => standard display\n            this.root.el.classList.remove(STICKY_CLASS);\n            this.lastScrollTop = 0;\n        }\n\n        this.oldScrollTop = scrollTop;\n    }\n\n    /**\n     * Allow to switch from the current view to another.\n     * Called when a view is clicked in the view switcher\n     * and reset mobile search state on switch view.\n     *\n     * @param {import(\"@web/views/view\").ViewType} viewType\n     */\n    switchView(viewType) {\n        this.resetSearchState();\n        this.actionService.switchView(viewType);\n    }\n\n    cycleThroughViews() {\n        const currentViewType = this.env.config.viewType;\n        const viewSwitcherEntries = this.env.config.viewSwitcherEntries;\n        const currentIndex = viewSwitcherEntries.findIndex(\n            (entry) => entry.type === currentViewType\n        );\n        const nextIndex = (currentIndex + 1) % viewSwitcherEntries.length;\n        this.switchView(viewSwitcherEntries[nextIndex].type);\n    }\n\n    /**\n     * @private\n     * @param {MouseEvent} ev\n     */\n    onWindowClick(ev) {\n        if (this.state.showViewSwitcher && !ev.target.closest(\".o_cp_switch_buttons\")) {\n            this.state.showViewSwitcher = false;\n        }\n    }\n\n    /**\n     * @param {KeyboardEvent} ev\n     */\n    onMainButtonsKeydown(ev) {\n        const hotkey = getActiveHotkey(ev);\n        if (hotkey === \"arrowdown\") {\n            this.env.searchModel.trigger(\"focus-view\");\n            ev.preventDefault();\n            ev.stopPropagation();\n        }\n    }\n\n    /**\n     * @param {EmbeddedAction} action\n     */\n    _checkValueLocalStorage(action) {\n        const actionIdStr = action.id.toString();\n        return this.state.embeddedInfos.visibleEmbeddedActions[actionIdStr];\n    }\n\n    /**\n     * The selected action is put into (or removed from) the localStorage and its visibility changes.\n     * The state variable visibleEmbeddedActions keeps track of the visible actions to avoid  having to parse\n     * the localStorage values every time we want to access them.\n     * @param {EmbeddedAction} action\n     */\n    _setVisibility(actionId) {\n        const actionIdStr = actionId.toString();\n        if (this.state.embeddedInfos.visibleEmbeddedActions[actionIdStr]) {\n            delete this.state.embeddedInfos.visibleEmbeddedActions[actionIdStr];\n        } else {\n            this.state.embeddedInfos.visibleEmbeddedActions[actionIdStr] = true;\n        }\n        browser.localStorage.setItem(\n            this.embeddedActionsVisibilityKey,\n            JSON.stringify(this.state.embeddedInfos.visibleEmbeddedActions)\n        );\n    }\n\n    _onShareCheckboxChange() {\n        this.state.embeddedInfos.newActionIsShared = !this.state.embeddedInfos.newActionIsShared;\n    }\n\n    /**\n     * @param {Event} ev\n     */\n    async _saveNewAction(ev) {\n        const {\n            newActionName,\n            newActionIsShared,\n            embeddedActions,\n            currentEmbeddedAction,\n            visibleEmbeddedActions,\n        } = this.state.embeddedInfos;\n        if (!newActionName) {\n            this.notificationService.add(_t(\"A name for your new action is required.\"), {\n                type: \"danger\",\n            });\n            ev.stopPropagation();\n            return this.newActionNameRef.el.focus();\n        }\n        const duplicateName = embeddedActions.some(({ name }) => name === newActionName);\n        if (duplicateName) {\n            this.notificationService.add(_t(\"An action with the same name already exists.\"), {\n                type: \"danger\",\n            });\n            ev.stopPropagation();\n            return this.newActionNameRef.el.focus();\n        }\n        const userId = newActionIsShared ? false : user.userId;\n\n        const {\n            parent_action_id,\n            action_id,\n            parent_res_model,\n            python_method,\n            domain,\n            context,\n            groups_ids,\n        } = currentEmbeddedAction;\n        const values = {\n            parent_action_id: parent_action_id[0],\n            parent_res_model,\n            parent_res_id: this.env.searchModel.globalContext.active_id,\n            user_id: userId,\n            is_deletable: true,\n            default_view_mode: this.env.config.viewType,\n            domain,\n            context,\n            groups_ids,\n            name: newActionName,\n        };\n        if (python_method) {\n            values.python_method = python_method;\n        } else {\n            values.action_id = action_id[0] || this.env.config.actionId;\n        }\n        const embeddedActionId = await this.orm.create(\"ir.embedded.actions\", [values]);\n        const description = `${newActionName}`;\n        this.env.searchModel.createNewFavorite({\n            description,\n            isDefault: true,\n            isShared: newActionIsShared,\n            embeddedActionId: embeddedActionId[0],\n        });\n        Object.assign(this.state.embeddedInfos, {\n            newActionName: \"\",\n            newActionIsShared: false,\n        });\n        const enrichedNewEmbeddedAction = {\n            ...values,\n            parent_action_id,\n            action_id,\n            id: embeddedActionId[0],\n        };\n        this.state.embeddedInfos.embeddedActions.push(enrichedNewEmbeddedAction);\n        const embeddedActionIdStr = embeddedActionId[0].toString();\n        visibleEmbeddedActions[embeddedActionIdStr] = true;\n        const order = this.state.embeddedInfos.embeddedActions.map((el) => el.id);\n        browser.localStorage.setItem(\n            this.embeddedActionsVisibilityKey,\n            JSON.stringify(visibleEmbeddedActions)\n        );\n        browser.localStorage.setItem(this.embeddedOrderKey, JSON.stringify(order));\n        this.env.config.setCurrentEmbeddedAction(embeddedActionId);\n        this.state.embeddedInfos.currentEmbeddedAction = enrichedNewEmbeddedAction;\n        this.state.embeddedInfos.newActionName = `${newActionName} Custom`;\n    }\n\n    /**\n     * @param {EmbeddedAction} action\n     */\n    openConfirmationDialog(action) {\n        const dialogProps = {\n            title: _t(\"Warning\"),\n            body: action.user_id\n                ? _t(\"Are you sure that you want to remove this embedded action?\")\n                : _t(\"This embedded action is global and will be removed for everyone.\"),\n            confirmLabel: _t(\"Delete\"),\n            confirm: async () => await this._deleteEmbeddedAction(action),\n            cancel: () => {},\n        };\n        this.dialogService.add(ConfirmationDialog, dialogProps);\n    }\n\n    /**\n     * @param {EmbeddedAction} action\n     */\n    async _deleteEmbeddedAction(action) {\n        const { visibleEmbeddedActions, embeddedActions, currentEmbeddedAction } =\n            this.state.embeddedInfos;\n        const actionIdStr = action.id.toString();\n        if (visibleEmbeddedActions[actionIdStr]) {\n            delete visibleEmbeddedActions[actionIdStr];\n        }\n        browser.localStorage.setItem(\n            this.embeddedActionsVisibilityKey,\n            JSON.stringify(visibleEmbeddedActions)\n        );\n        this.state.embeddedInfos.embeddedActions = embeddedActions.filter(\n            ({ id }) => id !== action.id\n        );\n        await this.orm.unlink(\"ir.embedded.actions\", [action.id]);\n        if (action.id === currentEmbeddedAction?.id) {\n            const { active_id, active_model } = this.env.searchModel.globalContext;\n            const actionContext = action.context ? makeContext([action.context]) : {};\n            const additionalContext = {\n                ...actionContext,\n                active_id,\n                active_model,\n            };\n            this.actionService.doAction(action.parent_action_id[0] || action.parent_action_id, {\n                additionalContext,\n                stackPosition: \"replaceCurrentAction\",\n            });\n        }\n    }\n\n    /**\n     * @param {EmbeddedAction} action\n     */\n    async onEmbeddedActionClick(action) {\n        this.env.config.setEmbeddedActions(this.state.embeddedInfos.embeddedActions);\n        const { active_id, active_model } = this.env.searchModel.globalContext;\n        const actionContext = action.context ? makeContext([action.context]) : {};\n        const context = {\n            ...actionContext,\n            active_id,\n            active_model,\n            current_embedded_action_id: action.id,\n            parent_action_embedded_actions: this.state.embeddedInfos.embeddedActions,\n            parent_action_id: action.parent_action_id[0] || action.parent_action_id,\n        };\n        this.actionService.doActionButton(\n            {\n                type: action.python_method ? \"object\" : \"action\",\n                resId: this.env.searchModel?.globalContext.active_id,\n                name: action.python_method || action.action_id[0] || action.action_id,\n                resModel: action.parent_res_model,\n                context,\n                stackPosition: this.env.config.parentActionId ? \"replaceCurrentAction\" : \"\",\n                viewType: action.default_view_mode,\n            },\n            { isEmbeddedAction: true }\n        );\n    }\n\n    /**\n     * @param {number[]} order\n     */\n    _sortEmbeddedActions(order) {\n        this.state.embeddedInfos.embeddedActions = this.state.embeddedInfos.embeddedActions.sort(\n            (a, b) => {\n                const indexA = order.indexOf(a.id);\n                if (!indexA) {\n                    return -1;\n                }\n                const indexB = order.indexOf(b.id);\n                if (!indexB) {\n                    return 1;\n                }\n                return indexA - indexB;\n            }\n        );\n    }\n\n    /**\n     * @param {Object} params\n     * @param {HTMLElement} params.element\n     */\n    _sortEmbeddedActionStart({ element, addClass }) {\n        addClass(element, \"o_dragged_embedded_action\");\n    }\n\n    /**\n     * @param {Object} params\n     * @param {HTMLElement} params.element\n     * @param {HTMLElement} params.previous\n     */\n    _sortEmbeddedActionDrop({ element, previous }) {\n        const order = this.state.embeddedInfos.embeddedActions.map((el) => el.id);\n        const elementId = Number(element.dataset.id) || false;\n        const elementIndex = order.indexOf(elementId);\n        order.splice(elementIndex, 1);\n        if (previous) {\n            const prevIndex = order.indexOf(Number(previous.dataset.id) || false);\n            order.splice(prevIndex + 1, 0, elementId);\n        } else {\n            order.splice(0, 0, elementId);\n        }\n        this._sortEmbeddedActions(order);\n        browser.localStorage.setItem(this.embeddedOrderKey, JSON.stringify(order));\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { AccordionItem } from \"@web/core/dropdown/accordion_item\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { Component, useRef, useState } from \"@odoo/owl\";\n\nconst favoriteMenuRegistry = registry.category(\"favoriteMenu\");\n\nexport class CustomFavoriteItem extends Component {\n    static template = \"web.CustomFavoriteItem\";\n    static components = { CheckBox, AccordionItem };\n    static props = {};\n\n    setup() {\n        this.notificationService = useService(\"notification\");\n        this.descriptionRef = useRef(\"description\");\n        this.state = useState({\n            description: this.env.config.getDisplayName(),\n            isDefault: false,\n            isShared: false,\n        });\n    }\n\n    /**\n     * @param {Event} ev\n     */\n    saveFavorite(ev) {\n        if (!this.state.description) {\n            this.notificationService.add(_t(\"A name for your favorite filter is required.\"), {\n                type: \"danger\",\n            });\n            ev.stopPropagation();\n            return this.descriptionRef.el.focus();\n        }\n        const favorites = this.env.searchModel.getSearchItems(\n            (s) => s.type === \"favorite\" && s.description === this.state.description\n        );\n        if (favorites.length) {\n            this.notificationService.add(_t(\"A filter with same name already exists.\"), {\n                type: \"danger\",\n            });\n            ev.stopPropagation();\n            return this.descriptionRef.el.focus();\n        }\n        const { description, isDefault, isShared } = this.state;\n        const embeddedActionId = this.env.config.currentEmbeddedActionId || false;\n        this.env.searchModel.createNewFavorite({\n            description,\n            isDefault,\n            isShared,\n            embeddedActionId,\n        });\n\n        Object.assign(this.state, {\n            description: this.env.config.getDisplayName(),\n            isDefault: false,\n            isShared: false,\n        });\n    }\n\n    /**\n     * @param {boolean} checked\n     */\n    onDefaultCheckboxChange(checked) {\n        this.state.isDefault = checked;\n        if (checked) {\n            this.state.isShared = false;\n        }\n    }\n\n    /**\n     * @param {boolean} checked\n     */\n    onShareCheckboxChange(checked) {\n        this.state.isShared = checked;\n        if (checked) {\n            this.state.isDefault = false;\n        }\n    }\n\n    /**\n     * @param {KeyboardEvent} ev\n     */\n    onInputKeydown(ev) {\n        switch (ev.key) {\n            case \"Enter\":\n                ev.preventDefault();\n                this.saveFavorite(ev);\n                break;\n            case \"Escape\":\n                // Gives the focus back to the component.\n                ev.preventDefault();\n                ev.target.blur();\n                break;\n        }\n    }\n}\n\nfavoriteMenuRegistry.add(\n    \"custom-favorite-item\",\n    { Component: CustomFavoriteItem, groupNumber: 3 },\n    { sequence: 0 }\n);\n", "import { Component } from \"@odoo/owl\";\n\nexport class CustomGroupByItem extends Component {\n    static template = \"web.CustomGroupByItem\";\n    static props = {\n        fields: Array,\n        onAddCustomGroup: Function,\n    };\n\n    get choices() {\n        return this.props.fields.map((f) => ({ label: f.string, value: f.name }));\n    }\n\n    onSelected(ev) {\n        if (ev.target.value) {\n            this.props.onAddCustomGroup(ev.target.value);\n            // reset the placeholder\n            ev.target.value = \"\";\n        }\n    }\n}\n", "import { Component, useRef } from \"@odoo/owl\";\nimport { ControlPanel } from \"@web/search/control_panel/control_panel\";\nimport { SearchPanel } from \"@web/search/search_panel/search_panel\";\n\n/**\n * @param {Object} params\n * @returns {Object}\n */\nexport function extractLayoutComponents(params) {\n    const layoutComponents = {\n        ControlPanel: params.ControlPanel || ControlPanel,\n        SearchPanel: params.SearchPanel || SearchPanel,\n    };\n    return layoutComponents;\n}\n\nexport class Layout extends Component {\n    static template = \"web.Layout\";\n    static props = {\n        className: { type: String, optional: true },\n        display: { type: Object, optional: true },\n        slots: { type: Object, optional: true },\n    };\n    static defaultProps = {\n        display: {},\n    };\n    setup() {\n        this.components = extractLayoutComponents(this.env.config);\n        this.contentRef = useRef(\"content\");\n    }\n    get controlPanelSlots() {\n        const slots = { ...this.props.slots };\n        if (this.env.inDialog) {\n            delete slots[\"layout-buttons\"];\n        }\n        delete slots.default;\n        return slots;\n    }\n}\n", "import { useEnv, useSubEnv, useState, onWillRender } from \"@odoo/owl\";\n\n/**\n * @typedef PagerUpdateParams\n * @property {number} offset\n * @property {number} limit\n */\n\n/**\n * @typedef PagerProps\n * @property {number} offset\n * @property {number} limit\n * @property {number} total\n * @property {(params: PagerUpdateParams) => any} onUpdate\n * @property {boolean} [isEditable]\n * @property {boolean} [withAccessKey]\n */\n\n/**\n * @param {() => PagerProps} getProps\n */\nexport function usePager(getProps) {\n    const env = useEnv();\n    const pagerState = useState({});\n\n    useSubEnv({\n        config: {\n            ...env.config,\n            pagerProps: pagerState,\n        },\n    });\n    onWillRender(() => {\n        Object.assign(pagerState, getProps() || { total: 0 });\n    });\n}\n", "import { AccordionItem, ACCORDION } from \"@web/core/dropdown/accordion_item\";\nimport { CheckboxItem } from \"@web/core/dropdown/checkbox_item\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { Component, useState, useChildSubEnv } from \"@odoo/owl\";\n\nexport class PropertiesGroupByItem extends Component {\n    static template = \"web.PropertiesGroupByItem\";\n    static components = { AccordionItem, CheckboxItem, DropdownItem };\n    static props = {\n        item: Object,\n        onGroup: Function,\n    };\n\n    setup() {\n        this.state = useState({ groupByItems: [] });\n        useChildSubEnv({\n            [ACCORDION]: {\n                accordionStateChanged: this.beforeOpen.bind(this),\n            },\n        });\n    }\n\n    /**\n     * The properties field is considered as active if one of its property is active.\n     */\n    get isActive() {\n        return this.state.groupByItems.some((item) => item.isActive);\n    }\n\n    /**\n     * True if all group items come from the same definition record.\n     */\n    get isSingleParent() {\n        const uniqueNames = new Set(this.state.groupByItems.map((item) => item.definitionRecordId));\n        return uniqueNames.size < 2;\n    }\n\n    /**\n     * Dynamically load the definition, only when needed (if we open the dropdown).\n     */\n    async beforeOpen() {\n        if (this.definitionLoaded) {\n            return;\n        }\n        this.definitionLoaded = true;\n\n        await this.env.searchModel.fillSearchViewItemsProperty();\n        this._updateGroupByItems();\n    }\n\n    /**\n     * Callback to group records per one property.\n     */\n    onGroup(ids) {\n        this.props.onGroup(ids);\n        this._updateGroupByItems(); // isActive state might have changed\n    }\n\n    /**\n     * Update the component state to sync it with the search model group item.\n     */\n    _updateGroupByItems() {\n        this.state.groupByItems = this.env.searchModel.getSearchItems(\n            (searchItem) =>\n                [\"groupBy\", \"dateGroupBy\"].includes(searchItem.type) &&\n                searchItem.isProperty &&\n                searchItem.propertyFieldName === this.props.item.fieldName\n        );\n    }\n}\n", "import { makeContext } from \"@web/core/context\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { evaluateBooleanExpr, evaluateExpr } from \"@web/core/py_js/py\";\nimport { clamp } from \"@web/core/utils/numbers\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { visitXML } from \"@web/core/utils/xml\";\nimport { DEFAULT_INTERVAL, toGeneratorId } from \"@web/search/utils/dates\";\n\nconst ALL = _t(\"All\");\nconst DEFAULT_LIMIT = 200;\nconst DEFAULT_VIEWS_WITH_SEARCH_PANEL = [\"kanban\", \"list\"];\n\n/**\n * Returns the split 'group_by' key from the given context attribute.\n * This helper accepts any invalid context or one that does not have\n * a valid 'group_by' key, and falls back to an empty list.\n * @param {string} context\n * @returns {string[]}\n */\nfunction getContextGroubBy(context) {\n    try {\n        return makeContext([context]).group_by?.split(\":\") || [];\n    } catch {\n        return [];\n    }\n}\n\nfunction reduceType(type) {\n    if (type === \"dateFilter\") {\n        return \"filter\";\n    }\n    if (type === \"dateGroupBy\") {\n        return \"groupBy\";\n    }\n    return type;\n}\n\nexport class SearchArchParser {\n    constructor(searchViewDescription, fields, searchDefaults = {}, searchPanelDefaults = {}) {\n        const { irFilters, arch } = searchViewDescription;\n\n        this.fields = fields || {};\n        this.irFilters = irFilters || [];\n        this.arch = arch || \"<search/>\";\n\n        this.labels = [];\n        this.preSearchItems = [];\n        this.searchPanelInfo = {\n            className: \"\",\n            fold: false,\n            viewTypes: DEFAULT_VIEWS_WITH_SEARCH_PANEL,\n        };\n        this.sections = [];\n\n        this.searchDefaults = searchDefaults;\n        this.searchPanelDefaults = searchPanelDefaults;\n\n        this.currentGroup = [];\n        this.currentTag = null;\n        this.groupNumber = 0;\n        this.pregroupOfGroupBys = [];\n\n        this.optionsParams = null;\n    }\n\n    parse() {\n        visitXML(this.arch, (node, visitChildren) => {\n            switch (node.tagName) {\n                case \"search\":\n                    this.visitSearch(node, visitChildren);\n                    break;\n                case \"searchpanel\":\n                    return this.visitSearchPanel(node);\n                case \"group\":\n                    this.visitGroup(node, visitChildren);\n                    break;\n                case \"separator\":\n                    this.visitSeparator();\n                    break;\n                case \"field\":\n                    this.visitField(node);\n                    break;\n                case \"filter\":\n                    if (this.optionsParams) {\n                        this.visitDateOption(node);\n                    } else {\n                        this.visitFilter(node, visitChildren);\n                    }\n                    break;\n            }\n        });\n\n        return {\n            labels: this.labels,\n            preSearchItems: this.preSearchItems,\n            searchPanelInfo: this.searchPanelInfo,\n            sections: this.sections,\n        };\n    }\n\n    pushGroup(tag = null) {\n        if (this.currentGroup.length) {\n            if (this.currentTag === \"groupBy\") {\n                this.pregroupOfGroupBys.push(...this.currentGroup);\n            } else {\n                this.preSearchItems.push(this.currentGroup);\n            }\n        }\n        this.currentTag = tag;\n        this.currentGroup = [];\n        this.groupNumber++;\n    }\n\n    visitField(node) {\n        this.pushGroup(\"field\");\n        const preField = { type: \"field\" };\n        if (node.hasAttribute(\"invisible\")) {\n            preField.invisible = node.getAttribute(\"invisible\");\n        }\n        if (node.hasAttribute(\"domain\")) {\n            preField.domain = node.getAttribute(\"domain\");\n        }\n        if (node.hasAttribute(\"filter_domain\")) {\n            preField.filterDomain = node.getAttribute(\"filter_domain\");\n        } else if (node.hasAttribute(\"operator\")) {\n            preField.operator = node.getAttribute(\"operator\");\n        }\n        if (node.hasAttribute(\"context\")) {\n            preField.context = node.getAttribute(\"context\");\n        }\n        if (node.hasAttribute(\"name\")) {\n            const name = node.getAttribute(\"name\");\n            if (!this.fields[name]) {\n                throw Error(`Unknown field ${name}`);\n            }\n            const fieldType = this.fields[name].type;\n            preField.fieldName = name;\n            preField.fieldType = fieldType;\n            if (fieldType !== \"properties\" && name in this.searchDefaults) {\n                preField.isDefault = true;\n                let value = this.searchDefaults[name];\n                value = Array.isArray(value) ? value[0] : value;\n                let operator = preField.operator;\n                if (!operator) {\n                    let type = fieldType;\n                    if (node.hasAttribute(\"widget\")) {\n                        type = node.getAttribute(\"widget\");\n                    }\n                    // Note: many2one as a default filter will have a\n                    // numeric value instead of a string => we want \"=\"\n                    // instead of \"ilike\".\n                    if ([\"char\", \"html\", \"many2many\", \"one2many\", \"text\"].includes(type)) {\n                        operator = \"ilike\";\n                    } else {\n                        operator = \"=\";\n                    }\n                }\n                preField.defaultRank = -10;\n                const { selection, context, relation } = this.fields[name];\n                preField.defaultAutocompleteValue = { label: `${value}`, operator, value };\n                if (fieldType === \"selection\") {\n                    const option = selection.find((sel) => sel[0] === value);\n                    if (!option) {\n                        throw Error();\n                    }\n                    preField.defaultAutocompleteValue.label = option[1];\n                } else if (fieldType === \"many2one\") {\n                    this.labels.push((orm) => {\n                        return orm\n                            .call(relation, \"read\", [value, [\"display_name\"]], { context })\n                            .then((results) => {\n                                preField.defaultAutocompleteValue.label =\n                                    results[0][\"display_name\"];\n                            });\n                    });\n                }\n            }\n        } else {\n            throw Error(); //but normally this should have caught earlier with view arch validation server side\n        }\n        if (node.hasAttribute(\"string\")) {\n            preField.description = node.getAttribute(\"string\");\n        } else if (preField.fieldName) {\n            preField.description = this.fields[preField.fieldName].string;\n        } else {\n            preField.description = \"\u03a9\";\n        }\n        this.currentGroup.push(preField);\n    }\n\n    visitFilter(node, visitChildren) {\n        const preSearchItem = { type: \"filter\" };\n        if (node.hasAttribute(\"context\")) {\n            const context = node.getAttribute(\"context\");\n            const [fieldName, defaultInterval] = getContextGroubBy(context);\n            const groupByField = this.fields[fieldName];\n            if (groupByField) {\n                preSearchItem.type = \"groupBy\";\n                preSearchItem.fieldName = fieldName;\n                preSearchItem.fieldType = groupByField.type;\n                if ([\"date\", \"datetime\"].includes(groupByField.type)) {\n                    preSearchItem.type = \"dateGroupBy\";\n                    preSearchItem.defaultIntervalId = defaultInterval || DEFAULT_INTERVAL;\n                }\n            } else {\n                preSearchItem.context = context;\n            }\n        }\n        if (reduceType(preSearchItem.type) !== this.currentTag) {\n            this.pushGroup(reduceType(preSearchItem.type));\n        }\n        if (preSearchItem.type === \"filter\") {\n            if (node.hasAttribute(\"date\")) {\n                const fieldName = node.getAttribute(\"date\");\n                preSearchItem.type = \"dateFilter\";\n                preSearchItem.fieldName = fieldName;\n                preSearchItem.fieldType = this.fields[fieldName].type;\n                const optionsParams = {\n                    startYear: Number(node.getAttribute(\"start_year\") || -2),\n                    endYear: Number(node.getAttribute(\"end_year\") || 0),\n                    startMonth: Number(node.getAttribute(\"start_month\") || -2),\n                    endMonth: Number(node.getAttribute(\"end_month\") || 0),\n                    customOptions: [],\n                };\n                const defaultOffset = clamp(optionsParams.startMonth, optionsParams.endMonth, 0);\n                preSearchItem.defaultGeneratorIds = [toGeneratorId(\"month\", defaultOffset)];\n                if (node.hasAttribute(\"default_period\")) {\n                    preSearchItem.defaultGeneratorIds = node\n                        .getAttribute(\"default_period\")\n                        .split(\",\");\n                }\n                this.optionsParams = optionsParams;\n                visitChildren();\n                preSearchItem.optionsParams = optionsParams;\n                this.optionsParams = null;\n            }\n            preSearchItem.domain = node.getAttribute(\"domain\") || \"[]\";\n        }\n        if (node.hasAttribute(\"invisible\")) {\n            preSearchItem.invisible = node.getAttribute(\"invisible\");\n            const fieldName = preSearchItem.fieldName;\n            if (fieldName && !this.fields[fieldName]) {\n                // In some case when a field is limited to specific groups\n                // on the model, we need to ensure to discard related filter\n                // as it may still be present in the view (in 'invisible' state)\n                return;\n            }\n        }\n        preSearchItem.groupNumber = this.groupNumber;\n        if (node.hasAttribute(\"name\")) {\n            const name = node.getAttribute(\"name\");\n            preSearchItem.name = name;\n            if (name in this.searchDefaults) {\n                preSearchItem.isDefault = true;\n                const value = this.searchDefaults[name];\n                if ([\"groupBy\", \"dateGroupBy\"].includes(preSearchItem.type)) {\n                    preSearchItem.defaultRank = typeof value === \"number\" ? value : 100;\n                } else {\n                    preSearchItem.defaultRank = -5;\n                }\n                if (\n                    preSearchItem.type === \"dateFilter\" &&\n                    typeof value === \"string\" &&\n                    !/^(true|1)$/i.test(value)\n                ) {\n                    preSearchItem.defaultGeneratorIds = value.split(\",\");\n                }\n            }\n        }\n        if (node.hasAttribute(\"string\")) {\n            preSearchItem.description = node.getAttribute(\"string\");\n        } else if (preSearchItem.fieldName) {\n            preSearchItem.description = this.fields[preSearchItem.fieldName].string;\n        } else if (node.hasAttribute(\"help\")) {\n            preSearchItem.description = node.getAttribute(\"help\");\n        } else if (node.hasAttribute(\"name\")) {\n            preSearchItem.description = node.getAttribute(\"name\");\n        } else {\n            preSearchItem.description = \"\u03a9\";\n        }\n        this.currentGroup.push(preSearchItem);\n    }\n\n    visitDateOption(node) {\n        const preDateOption = { type: \"dateOption\" };\n        for (const attribute of [\"name\", \"string\", \"domain\"]) {\n            if (!node.getAttribute(attribute)) {\n                throw new Error(`Attribute \"${attribute}\" is missing.`);\n            }\n        }\n        preDateOption.id = `custom_${node.getAttribute(\"name\")}`;\n        preDateOption.description = node.getAttribute(\"string\");\n        preDateOption.domain = node.getAttribute(\"domain\");\n        this.optionsParams.customOptions.push(preDateOption);\n    }\n\n    visitGroup(node, visitChildren) {\n        this.pushGroup();\n        visitChildren();\n        this.pushGroup();\n    }\n\n    visitSearch(node, visitChildren) {\n        visitChildren();\n        this.pushGroup();\n        if (this.pregroupOfGroupBys.length) {\n            this.preSearchItems.push(this.pregroupOfGroupBys);\n        }\n    }\n\n    visitSearchPanel(searchPanelNode) {\n        let hasCategoryWithCounters = false;\n        let hasFilterWithDomain = false;\n        let nextSectionId = 1;\n\n        if (searchPanelNode.hasAttribute(\"class\")) {\n            this.searchPanelInfo.className = searchPanelNode.getAttribute(\"class\");\n        }\n        if (searchPanelNode.hasAttribute(\"fold\")) {\n            this.searchPanelInfo.fold = exprToBoolean(searchPanelNode.getAttribute(\"fold\"));\n        }\n        if (searchPanelNode.hasAttribute(\"view_types\")) {\n            this.searchPanelInfo.viewTypes = searchPanelNode.getAttribute(\"view_types\").split(\",\");\n        }\n\n        for (const node of searchPanelNode.children) {\n            if (node.nodeType !== 1 || node.tagName !== \"field\") {\n                continue;\n            }\n            if (\n                node.getAttribute(\"invisible\") === \"True\" ||\n                node.getAttribute(\"invisible\") === \"1\"\n            ) {\n                continue;\n            }\n            const attrs = {};\n            for (const attrName of node.getAttributeNames()) {\n                attrs[attrName] = node.getAttribute(attrName);\n            }\n\n            const type = attrs.select === \"multi\" ? \"filter\" : \"category\";\n            const section = {\n                color: attrs.color || null,\n                description: attrs.string || this.fields[attrs.name].string,\n                enableCounters: evaluateBooleanExpr(attrs.enable_counters),\n                expand: evaluateBooleanExpr(attrs.expand),\n                fieldName: attrs.name,\n                icon: attrs.icon || null,\n                id: nextSectionId++,\n                limit: evaluateExpr(attrs.limit || String(DEFAULT_LIMIT)),\n                type,\n                values: new Map(),\n            };\n            if (type === \"category\") {\n                section.activeValueId = this.searchPanelDefaults[attrs.name];\n                section.icon = section.icon || \"fa-folder\";\n                section.hierarchize = evaluateBooleanExpr(attrs.hierarchize || \"1\");\n                section.values.set(false, {\n                    childrenIds: [],\n                    display_name: ALL.toString(),\n                    id: false,\n                    bold: true,\n                    parentId: false,\n                });\n                hasCategoryWithCounters = hasCategoryWithCounters || section.enableCounters;\n            } else {\n                section.domain = attrs.domain || \"[]\";\n                section.groupBy = attrs.groupby || null;\n                section.icon = section.icon || \"fa-filter\";\n                hasFilterWithDomain = hasFilterWithDomain || section.domain !== \"[]\";\n            }\n            this.sections.push([section.id, section]);\n        }\n\n        /**\n         * Category counters are automatically disabled if a filter domain is found\n         * to avoid inconsistencies with the counters. The underlying problem could\n         * actually be solved by reworking the search panel and the way the\n         * counters are computed, though this is not the current priority\n         * considering the time it would take, hence this quick \"fix\".\n         */\n        if (hasCategoryWithCounters && hasFilterWithDomain) {\n            // If incompatibilities are found -> disables all category counters\n            for (const section of this.sections) {\n                if (section.type === \"category\") {\n                    section.enableCounters = false;\n                }\n            }\n            // ... and triggers a warning\n            console.warn(\n                \"Warning: categories with counters are incompatible with filters having a domain attribute.\",\n                \"All category counters have been disabled to avoid inconsistencies.\"\n            );\n        }\n\n        return false; // we do not want to let the parser keep visiting children\n    }\n\n    visitSeparator() {\n        this.pushGroup();\n    }\n}\n", "import { Domain } from \"@web/core/domain\";\nimport { serializeDate, serializeDateTime } from \"@web/core/l10n/dates\";\nimport { registry } from \"@web/core/registry\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { useAutofocus, useBus, useService } from \"@web/core/utils/hooks\";\nimport { DomainSelectorDialog } from \"@web/core/domain_selector_dialog/domain_selector_dialog\";\nimport { fuzzyTest } from \"@web/core/utils/search\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { SearchBarMenu } from \"../search_bar_menu/search_bar_menu\";\n\nimport { Component, useExternalListener, useRef, useState } from \"@odoo/owl\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { hasTouch } from \"@web/core/browser/feature_detection\";\nconst parsers = registry.category(\"parsers\");\n\nconst CHAR_FIELDS = [\"char\", \"html\", \"many2many\", \"many2one\", \"one2many\", \"text\", \"properties\"];\nconst FOLDABLE_TYPES = [\"properties\", \"many2one\", \"many2many\"];\n\nlet nextItemId = 1;\nconst SUB_ITEMS_DEFAULT_LIMIT = 8;\n\nexport class SearchBar extends Component {\n    static template = \"web.SearchBar\";\n    static components = {\n        SearchBarMenu,\n    };\n    static props = {\n        autofocus: { type: Boolean, optional: true },\n        slots: {\n            type: Object,\n            optional: true,\n            shape: {\n                default: { optional: true },\n                \"search-bar-additional-menu\": { optional: true },\n            },\n        },\n        toggler: {\n            type: Object,\n            optional: true,\n        },\n    };\n    static defaultProps = {\n        autofocus: true,\n    };\n\n    setup() {\n        this.dialogService = useService(\"dialog\");\n        this.fields = this.env.searchModel.searchViewFields;\n        this.searchItemsFields = this.env.searchModel.getSearchItems((f) => f.type === \"field\");\n        this.root = useRef(\"root\");\n        this.ui = useService(\"ui\");\n\n        this.visibilityState = useState(this.props.toggler?.state || { showSearchBar: true });\n\n        // core state\n        this.state = useState({\n            expanded: [],\n            focusedIndex: 0,\n            query: \"\",\n            subItemsLimits: {},\n        });\n\n        // derived state\n        this.items = useState([]);\n        this.subItems = {};\n\n        this.searchBarDropdownState = useDropdownState();\n\n        this.orm = useService(\"orm\");\n\n        this.keepLast = new KeepLast();\n\n        this.inputRef =\n            this.env.config.disableSearchBarAutofocus || !this.props.autofocus\n                ? useRef(\"autofocus\")\n                : useAutofocus({ mobile: this.props.toggler !== undefined }); // only force the focus on touch devices when the toggler is present on small devices\n\n        useBus(this.env.searchModel, \"focus-search\", () => {\n            this.inputRef.el.focus();\n        });\n\n        useBus(this.env.searchModel, \"update\", this.render);\n\n        useExternalListener(window, \"click\", this.onWindowClick);\n        useExternalListener(window, \"keydown\", this.onWindowKeydown);\n    }\n\n    /**\n     * @param {number} id\n     * @param {Object}\n     */\n    getSearchItem(id) {\n        return this.env.searchModel.searchItems[id];\n    }\n\n    /**\n     * @param {Object} [options={}]\n     * @param {number[]} [options.expanded]\n     * @param {number} [options.focusedIndex]\n     * @param {string} [options.query]\n     * @param {Object[]} [options.subItems]\n     * @returns {Object[]}\n     */\n    async computeState(options = {}) {\n        const query = \"query\" in options ? options.query : this.state.query;\n        const expanded = \"expanded\" in options ? options.expanded : this.state.expanded;\n        const focusedIndex =\n            \"focusedIndex\" in options ? options.focusedIndex : this.state.focusedIndex;\n        const subItems = \"subItems\" in options ? options.subItems : this.subItems;\n\n        const tasks = [];\n        for (const id of expanded) {\n            const searchItem = this.getSearchItem(id);\n            if (searchItem.type === \"field\" && searchItem.fieldType === \"properties\") {\n                tasks.push({ id, prom: this.getSearchItemsProperties(searchItem) });\n            } else if (!subItems[id]) {\n                if (!this.state.subItemsLimits[id]) {\n                    this.state.subItemsLimits[id] = SUB_ITEMS_DEFAULT_LIMIT;\n                }\n                tasks.push({ id, prom: this.computeSubItems(searchItem, query) });\n            }\n        }\n\n        const prom = this.keepLast.add(Promise.all(tasks.map((task) => task.prom)));\n\n        if (tasks.length) {\n            const taskResults = await prom;\n            tasks.forEach((task, index) => {\n                subItems[task.id] = taskResults[index];\n            });\n        }\n\n        this.state.expanded = expanded;\n        this.state.query = query;\n        this.state.focusedIndex = focusedIndex;\n        this.subItems = subItems;\n\n        this.inputRef.el.value = query;\n\n        const trimmedQuery = this.state.query.trim();\n\n        this.items.length = 0;\n        if (!trimmedQuery) {\n            return;\n        }\n\n        for (const searchItem of this.searchItemsFields) {\n            this.items.push(...this.getItems(searchItem, trimmedQuery));\n        }\n\n        this.items.push({\n            title: _t(\"Add a custom filter\"),\n            isAddCustomFilterButton: true,\n        });\n    }\n\n    /**\n     * @param {Object} searchItem\n     * @param {string} trimmedQuery\n     * @returns {Object[]}\n     */\n    getItems(searchItem, trimmedQuery) {\n        const items = [];\n\n        const isFieldProperty = searchItem.type === \"field_property\";\n        const fieldType = this.getFieldType(searchItem);\n\n        /** @todo do something with respect to localization (rtl) */\n        let preposition = this.getPreposition(searchItem);\n\n        if ((isFieldProperty && FOLDABLE_TYPES.includes(fieldType)) || fieldType === \"properties\") {\n            // Do not chose preposition for foldable properties\n            // or the properties item itself\n            preposition = null;\n        }\n\n        if ([\"selection\", \"boolean\", \"tags\"].includes(fieldType)) {\n            const booleanOptions = [\n                [true, _t(\"Yes\")],\n                [false, _t(\"No\")],\n            ];\n            let options;\n            if (isFieldProperty) {\n                const { selection, tags } = searchItem.propertyFieldDefinition || {};\n                options = selection || tags || booleanOptions;\n            } else {\n                options = this.fields[searchItem.fieldName].selection || booleanOptions;\n            }\n            for (const [value, label] of options) {\n                if (fuzzyTest(trimmedQuery.toLowerCase(), label.toLowerCase())) {\n                    items.push({\n                        id: nextItemId++,\n                        searchItemDescription: searchItem.description,\n                        preposition,\n                        searchItemId: searchItem.id,\n                        label,\n                        /** @todo check if searchItem.operator is fine (here and elsewhere) */\n                        operator: searchItem.operator || \"=\",\n                        value,\n                        isFieldProperty,\n                    });\n                }\n            }\n            return items;\n        }\n\n        const parser = parsers.contains(fieldType) ? parsers.get(fieldType) : (str) => str;\n        let value;\n        try {\n            switch (fieldType) {\n                case \"date\": {\n                    value = serializeDate(parser(trimmedQuery));\n                    break;\n                }\n                case \"datetime\": {\n                    value = serializeDateTime(parser(trimmedQuery));\n                    break;\n                }\n                case \"many2one\": {\n                    value = trimmedQuery;\n                    break;\n                }\n                default: {\n                    value = parser(trimmedQuery);\n                }\n            }\n        } catch {\n            return [];\n        }\n\n        const item = {\n            id: nextItemId++,\n            searchItemDescription: searchItem.description,\n            preposition,\n            searchItemId: searchItem.id,\n            label: this.state.query,\n            operator: searchItem.operator || (CHAR_FIELDS.includes(fieldType) ? \"ilike\" : \"=\"),\n            value,\n            isFieldProperty,\n        };\n\n        if (isFieldProperty) {\n            item.isParent = FOLDABLE_TYPES.includes(fieldType);\n            item.unselectable = FOLDABLE_TYPES.includes(fieldType);\n            item.propertyItemId = searchItem.propertyItemId;\n        } else if (fieldType === \"properties\") {\n            item.isParent = true;\n            item.unselectable = true;\n        } else if (fieldType === \"many2one\") {\n            item.isParent = true;\n        }\n\n        if (item.isParent) {\n            item.isExpanded = this.state.expanded.includes(item.searchItemId);\n        }\n\n        items.push(item);\n\n        if (item.isExpanded) {\n            if (searchItem.type === \"field\" && searchItem.fieldType === \"properties\") {\n                for (const subItem of this.subItems[searchItem.id]) {\n                    items.push(...this.getItems(subItem, trimmedQuery));\n                }\n            } else {\n                items.push(...this.subItems[searchItem.id]);\n            }\n        }\n\n        return items;\n    }\n\n    getPreposition(searchItem) {\n        const fieldType = this.getFieldType(searchItem);\n        return [\"date\", \"datetime\"].includes(fieldType) ? _t(\"at\") : _t(\"for\");\n    }\n\n    getFieldType(searchItem) {\n        const { type } =\n            searchItem.type === \"field_property\"\n                ? searchItem.propertyFieldDefinition\n                : this.fields[searchItem.fieldName];\n        const fieldType = type === \"reference\" ? \"char\" : type;\n\n        return fieldType;\n    }\n\n    /**\n     * @param {Object} searchItem\n     * @returns {Object[]}\n     */\n    getSearchItemsProperties(searchItem) {\n        return this.env.searchModel.getSearchItemsProperties(searchItem);\n    }\n\n    /**\n     * @param {Object} searchItem\n     * @param {string} query\n     * @returns {Object[]}\n     */\n    async computeSubItems(searchItem, query) {\n        const field = this.fields[searchItem.fieldName];\n        const context = { ...this.env.searchModel.domainEvalContext, ...field.context };\n        let domain = [];\n        if (searchItem.domain) {\n            try {\n                domain = new Domain(searchItem.domain).toList(context);\n            } catch {\n                // Pass\n            }\n        }\n        const relation =\n            searchItem.type === \"field_property\"\n                ? searchItem.propertyFieldDefinition.comodel\n                : field.relation;\n\n        let nameSearchOperator = \"ilike\";\n        if (query && query[0] === '\"' && query[query.length - 1] === '\"') {\n            query = query.slice(1, -1);\n            nameSearchOperator = \"=\";\n        }\n        const limitToFetch = this.state.subItemsLimits[searchItem.id] + 1;\n        const options = await this.orm.call(relation, \"name_search\", [], {\n            args: domain,\n            operator: nameSearchOperator,\n            context,\n            limit: limitToFetch,\n            name: query.trim(),\n        });\n\n        let showLoadMore = false;\n        if (options.length === limitToFetch) {\n            options.pop();\n            showLoadMore = true;\n        }\n\n        const subItems = [];\n        if (options.length) {\n            const operator = searchItem.operator || \"=\";\n            for (const [value, label] of options) {\n                subItems.push({\n                    id: nextItemId++,\n                    isChild: true,\n                    searchItemId: searchItem.id,\n                    value,\n                    label,\n                    operator,\n                });\n            }\n            if (showLoadMore) {\n                subItems.push({\n                    id: nextItemId++,\n                    isChild: true,\n                    searchItemId: searchItem.id,\n                    label: _t(\"Load more\"),\n                    unselectable: true,\n                    loadMore: () => {\n                        this.state.subItemsLimits[searchItem.id] += SUB_ITEMS_DEFAULT_LIMIT;\n                        const newSubItems = [...this.subItems];\n                        newSubItems[searchItem.id] = undefined;\n                        this.computeState({ subItems: newSubItems });\n                    },\n                });\n            }\n        } else {\n            subItems.push({\n                id: nextItemId++,\n                isChild: true,\n                searchItemId: searchItem.id,\n                label: _t(\"(no result)\"),\n                unselectable: true,\n            });\n        }\n        return subItems;\n    }\n\n    /**\n     * @param {number} [index]\n     */\n    focusFacet(index) {\n        const facets = this.root.el.getElementsByClassName(\"o_searchview_facet\");\n        if (facets.length) {\n            if (index === undefined) {\n                facets[facets.length - 1].focus();\n            } else {\n                facets[index].focus();\n            }\n        }\n    }\n\n    /**\n     * @param {Object} facet\n     */\n    removeFacet(facet) {\n        this.env.searchModel.deactivateGroup(facet.groupId);\n        this.inputRef.el.focus();\n    }\n\n    resetState(options = { focus: true }) {\n        this.state.subItemsLimits = {};\n        this.computeState({ expanded: [], focusedIndex: 0, query: \"\", subItems: [] });\n        if (options.focus) {\n            this.inputRef.el.focus();\n        }\n    }\n\n    /**\n     * @param {Object} item\n     */\n    selectItem(item) {\n        if (item.isAddCustomFilterButton) {\n            return this.env.searchModel.spawnCustomFilterDialog();\n        }\n\n        const searchItem = this.getSearchItem(item.searchItemId);\n        if (\n            (searchItem.type === \"field\" && searchItem.fieldType === \"properties\") ||\n            (searchItem.type === \"field_property\" && item.unselectable)\n        ) {\n            this.toggleItem(item, !item.isExpanded);\n            return;\n        }\n\n        if (!item.unselectable) {\n            const { searchItemId, label, operator, value } = item;\n            const autoCompleteValues = { label, operator, value };\n            if (value && value[0] === '\"' && value[value.length - 1] === '\"') {\n                autoCompleteValues.value = value.slice(1, -1);\n                autoCompleteValues.label = label.slice(1, -1);\n                autoCompleteValues.operator = \"=\";\n                autoCompleteValues.enforceEqual = true;\n            }\n            this.env.searchModel.addAutoCompletionValues(searchItemId, autoCompleteValues);\n        }\n\n        if (item.loadMore) {\n            item.loadMore();\n        } else {\n            this.resetState();\n        }\n    }\n\n    /**\n     * @param {Object} item\n     * @param {boolean} shouldExpand\n     */\n    toggleItem(item, shouldExpand) {\n        const id = item.searchItemId;\n        const expanded = [...this.state.expanded];\n        const index = expanded.findIndex((id0) => id0 === id);\n        if (shouldExpand === true) {\n            if (index < 0) {\n                expanded.push(id);\n            }\n        } else {\n            if (index >= 0) {\n                expanded.splice(index, 1);\n            }\n        }\n        this.computeState({ expanded });\n    }\n\n    //---------------------------------------------------------------------\n    // Handlers\n    //---------------------------------------------------------------------\n\n    onFacetLabelClick(target, facet) {\n        const { domain, groupId } = facet;\n        if (this.env.searchModel.canOrderByCount && facet.type === \"groupBy\") {\n            this.env.searchModel.switchGroupBySort();\n            return;\n        } else if (!domain) {\n            return;\n        }\n        const { resModel } = this.env.searchModel;\n        this.dialogService.add(DomainSelectorDialog, {\n            resModel,\n            domain,\n            context: this.env.searchModel.domainEvalContext,\n            onConfirm: (domain) => this.env.searchModel.splitAndAddDomain(domain, groupId),\n            disableConfirmButton: (domain) => domain === `[]`,\n            title: _t(\"Modify Condition\"),\n            isDebugMode: this.env.searchModel.isDebugMode,\n        });\n    }\n\n    /**\n     * @param {Object} facet\n     * @param {number} facetIndex\n     * @param {KeyboardEvent} ev\n     */\n    onFacetKeydown(facet, facetIndex, ev) {\n        switch (ev.key) {\n            case \"ArrowLeft\": {\n                if (facetIndex === 0) {\n                    this.inputRef.el.focus();\n                } else {\n                    this.focusFacet(facetIndex - 1);\n                }\n                break;\n            }\n            case \"ArrowRight\": {\n                const facets = this.root.el.getElementsByClassName(\"o_searchview_facet\");\n                if (facetIndex === facets.length - 1) {\n                    this.inputRef.el.focus();\n                } else {\n                    this.focusFacet(facetIndex + 1);\n                }\n                break;\n            }\n            case \"Backspace\": {\n                this.removeFacet(facet);\n                break;\n            }\n        }\n    }\n\n    /**\n     * @param {Object} facet\n     */\n    onFacetRemove(facet) {\n        this.removeFacet(facet);\n    }\n\n    /**\n     * @param {number} index\n     */\n    onItemMousemove(focusedIndex) {\n        this.state.focusedIndex = focusedIndex;\n        this.inputRef.el.focus();\n    }\n\n    /**\n     * @param {KeyboardEvent} ev\n     */\n    onSearchKeydown(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 focusedItem = this.items[this.state.focusedIndex];\n        let focusedIndex;\n        switch (ev.key) {\n            case \"ArrowDown\":\n                ev.preventDefault();\n                if (this.items.length) {\n                    if (this.state.focusedIndex >= this.items.length - 1) {\n                        focusedIndex = 0;\n                    } else {\n                        focusedIndex = this.state.focusedIndex + 1;\n                    }\n                } else {\n                    this.env.searchModel.trigger(\"focus-view\");\n                }\n                break;\n            case \"ArrowUp\":\n                ev.preventDefault();\n                if (this.items.length) {\n                    if (\n                        this.state.focusedIndex === 0 ||\n                        this.state.focusedIndex > this.items.length - 1\n                    ) {\n                        focusedIndex = this.items.length - 1;\n                    } else {\n                        focusedIndex = this.state.focusedIndex - 1;\n                    }\n                }\n                break;\n            case \"ArrowLeft\":\n                if (focusedItem && focusedItem.isParent && focusedItem.isExpanded) {\n                    ev.preventDefault();\n                    this.toggleItem(focusedItem, false);\n                } else if (focusedItem && focusedItem.isChild) {\n                    ev.preventDefault();\n                    focusedIndex = this.items.findIndex(\n                        (item) => item.isParent && item.searchItemId === focusedItem.searchItemId\n                    );\n                } else if (focusedItem && focusedItem.isFieldProperty) {\n                    ev.preventDefault();\n                    focusedIndex = this.items.findIndex(\n                        (item) => item.isParent && item.searchItemId === focusedItem.propertyItemId\n                    );\n                } else if (ev.target.selectionStart === 0) {\n                    // focus rightmost facet if any.\n                    this.focusFacet();\n                } else {\n                    // do nothing and navigate inside text\n                }\n                break;\n            case \"ArrowRight\":\n                if (ev.target.selectionStart === this.state.query.length) {\n                    if (focusedItem && focusedItem.isParent) {\n                        ev.preventDefault();\n                        if (focusedItem.isExpanded) {\n                            focusedIndex = this.state.focusedIndex + 1;\n                        } else if (!ev.repeat) {\n                            this.toggleItem(focusedItem, true);\n                        }\n                    } else if (ev.target.selectionStart === this.state.query.length) {\n                        // Priority 3: focus leftmost facet if any.\n                        this.focusFacet(0);\n                    }\n                }\n                break;\n            case \"Backspace\":\n                if (!this.state.query.length) {\n                    const facets = this.env.searchModel.facets;\n                    if (facets.length) {\n                        this.removeFacet(facets[facets.length - 1]);\n                    }\n                }\n                break;\n            case \"Enter\":\n                if (!this.state.query.length) {\n                    this.env.searchModel.search(); /** @todo keep this thing ?*/\n                    break;\n                } else if (focusedItem) {\n                    ev.preventDefault(); // keep the focus inside the search bar\n                    this.selectItem(focusedItem);\n                }\n                break;\n            case \"Tab\":\n                if (this.state.query.length && focusedItem) {\n                    ev.preventDefault(); // keep the focus inside the search bar\n                    this.selectItem(focusedItem);\n                }\n                break;\n            case \"Escape\":\n                this.resetState();\n                break;\n        }\n\n        if (focusedIndex !== undefined) {\n            this.state.focusedIndex = focusedIndex;\n        }\n    }\n\n    onSearchClick() {\n        if (!hasTouch() && !this.inputRef.el.value.length) {\n            this.searchBarDropdownState.open();\n        }\n    }\n\n    /**\n     * @param {InputEvent} ev\n     */\n    onSearchInput(ev) {\n        if (!hasTouch()) {\n            this.searchBarDropdownState.close();\n        }\n        const query = ev.target.value;\n        if (query.trim()) {\n            this.computeState({ query, expanded: [], focusedIndex: 0, subItems: [] });\n        } else if (this.items.length) {\n            this.resetState();\n        }\n    }\n\n    onClickSearchIcon() {\n        const focusedItem = this.items[this.state.focusedIndex];\n        if (!this.state.query.length) {\n            this.env.searchModel.search();\n        } else if (focusedItem) {\n            this.selectItem(focusedItem);\n        }\n    }\n\n    onToggleSearchBar() {\n        this.state.showSearchBar = !this.state.showSearchBar;\n    }\n\n    /**\n     * @param {MouseEvent} ev\n     */\n    onWindowClick(ev) {\n        if (this.items.length && !this.root.el.contains(ev.target)) {\n            this.resetState({ focus: false });\n        }\n    }\n\n    /**\n     * @param {KeyboardEvent} ev\n     */\n    onWindowKeydown(ev) {\n        if (this.items.length && ev.key === \"Escape\") {\n            this.resetState();\n        }\n    }\n}\n", "import { Component, useEffect, useState } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useDebounced } from \"@web/core/utils/timing\";\n\nexport class SearchBarToggler extends Component {\n    static template = \"web.SearchBar.Toggler\";\n    static props = {\n        isSmall: Boolean,\n        showSearchBar: Boolean,\n        toggleSearchBar: Function,\n    };\n}\n\nexport function useSearchBarToggler() {\n    const ui = useService(\"ui\");\n\n    let isToggled = false;\n    const state = useState({\n        isSmall: ui.isSmall,\n        showSearchBar: false,\n    });\n    const updateState = () => {\n        state.isSmall = ui.isSmall;\n        state.showSearchBar = !ui.isSmall || isToggled;\n    };\n    updateState();\n\n    function toggleSearchBar() {\n        isToggled = !isToggled;\n        updateState();\n    }\n\n    const onResize = useDebounced(updateState, 200);\n    useEffect(\n        () => {\n            browser.addEventListener(\"resize\", onResize);\n            return () => browser.removeEventListener(\"resize\", onResize);\n        },\n        () => []\n    );\n\n    return {\n        state,\n        component: SearchBarToggler,\n        get props() {\n            return {\n                isSmall: state.isSmall,\n                showSearchBar: state.showSearchBar,\n                toggleSearchBar,\n            };\n        },\n    };\n}\n", "import { Component } from \"@odoo/owl\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { PropertiesGroupByItem } from \"@web/search/properties_group_by_item/properties_group_by_item\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { registry } from \"@web/core/registry\";\nimport { sortBy } from \"@web/core/utils/arrays\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { AccordionItem } from \"@web/core/dropdown/accordion_item\";\nimport { CustomGroupByItem } from \"@web/search/custom_group_by_item/custom_group_by_item\";\nimport { CheckboxItem } from \"@web/core/dropdown/checkbox_item\";\nimport { FACET_ICONS, GROUPABLE_TYPES } from \"@web/search/utils/misc\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nconst favoriteMenuRegistry = registry.category(\"favoriteMenu\");\n\nexport class SearchBarMenu extends Component {\n    static template = \"web.SearchBarMenu\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n        CheckboxItem,\n        CustomGroupByItem,\n        AccordionItem,\n        PropertiesGroupByItem,\n    };\n    static props = {\n        slots: {\n            type: Object,\n            optional: true,\n            shape: {\n                default: { optional: true },\n            },\n        },\n        dropdownState: {\n            type: Object,\n            optional: true,\n            shape: {\n                isOpen: Boolean,\n                open: Function,\n                close: Function,\n            },\n        },\n    };\n\n    setup() {\n        this.facet_icons = FACET_ICONS;\n        // Filter\n        this.dialogService = useService(\"dialog\");\n        // GroupBy\n        const fields = [];\n        for (const [fieldName, field] of Object.entries(this.env.searchModel.searchViewFields)) {\n            if (this.validateField(fieldName, field)) {\n                fields.push(Object.assign({ name: fieldName }, field));\n            }\n        }\n        this.fields = sortBy(fields, \"string\");\n        // Favorite\n        useBus(this.env.searchModel, \"update\", this.render);\n    }\n\n    // Filter Panel\n    get filterItems() {\n        return this.env.searchModel.getSearchItems((searchItem) =>\n            [\"filter\", \"dateFilter\"].includes(searchItem.type)\n        );\n    }\n\n    async onAddCustomFilterClick() {\n        this.env.searchModel.spawnCustomFilterDialog();\n    }\n\n    /**\n     * @param {Object} param0\n     * @param {number} param0.itemId\n     * @param {number} [param0.optionId]\n     */\n    onFilterSelected({ itemId, optionId }) {\n        if (optionId) {\n            this.env.searchModel.toggleDateFilter(itemId, optionId);\n        } else {\n            this.env.searchModel.toggleSearchItem(itemId);\n        }\n    }\n\n    // GroupBy Panel\n    /**\n     * @returns {boolean}\n     */\n    get hideCustomGroupBy() {\n        return this.env.searchModel.hideCustomGroupBy || false;\n    }\n\n    /**\n     * @returns {Object[]}\n     */\n    get groupByItems() {\n        return this.env.searchModel.getSearchItems(\n            (searchItem) =>\n                [\"groupBy\", \"dateGroupBy\"].includes(searchItem.type) && !searchItem.isProperty\n        );\n    }\n\n    /**\n     * @param {string} fieldName\n     * @param {Object} field\n     * @returns {boolean}\n     */\n    validateField(fieldName, field) {\n        const { groupable, type } = field;\n        return groupable && fieldName !== \"id\" && GROUPABLE_TYPES.includes(type);\n    }\n\n    /**\n     * @param {Object} param0\n     * @param {number} param0.itemId\n     * @param {number} [param0.optionId]\n     */\n    onGroupBySelected({ itemId, optionId }) {\n        if (optionId) {\n            this.env.searchModel.toggleDateGroupBy(itemId, optionId);\n        } else {\n            this.env.searchModel.toggleSearchItem(itemId);\n        }\n    }\n\n    /**\n     * @param {string} fieldName\n     */\n    onAddCustomGroup(fieldName) {\n        this.env.searchModel.createNewGroupBy(fieldName);\n    }\n\n    // Comparison Panel\n    get showComparisonMenu() {\n        return (\n            this.env.searchModel.searchMenuTypes.has(\"comparison\") &&\n            this.env.searchModel.getSearchItems((i) => i.type === \"comparison\").length > 0\n        );\n    }\n    get comparisonItems() {\n        return this.env.searchModel.getSearchItems(\n            (searchItem) => searchItem.type === \"comparison\"\n        );\n    }\n\n    /**\n     * @param {number} itemId\n     */\n    onComparisonSelected(itemId) {\n        this.env.searchModel.toggleSearchItem(itemId);\n    }\n\n    // Favorite Panel\n\n    get favorites() {\n        return this.env.searchModel.getSearchItems(\n            (searchItem) => searchItem.type === \"favorite\" && searchItem.userId !== false\n        );\n    }\n\n    get sharedFavorites() {\n        return this.env.searchModel.getSearchItems(\n            (searchItem) => searchItem.type === \"favorite\" && searchItem.userId === false\n        );\n    }\n\n    get otherItems() {\n        const registryMenus = [];\n        for (const item of favoriteMenuRegistry.getAll()) {\n            if (\"isDisplayed\" in item ? item.isDisplayed(this.env) : true) {\n                registryMenus.push({\n                    Component: item.Component,\n                    groupNumber: item.groupNumber,\n                    key: item.Component.name,\n                });\n            }\n        }\n        return registryMenus;\n    }\n\n    onFavoriteSelected(itemId) {\n        this.env.searchModel.toggleSearchItem(itemId);\n    }\n\n    openConfirmationDialog(itemId, userId) {\n        const dialogProps = {\n            title: _t(\"Warning\"),\n            body: userId\n                ? _t(\"Are you sure that you want to remove this filter?\")\n                : _t(\"This filter is global and will be removed for everyone.\"),\n            confirmLabel: _t(\"Delete Filter\"),\n            confirm: () => this.env.searchModel.deleteFavorite(itemId),\n            cancel: () => {},\n        };\n        this.dialogService.add(ConfirmationDialog, dialogProps);\n    }\n}\n", "import { makeContext } from \"@web/core/context\";\nimport { Domain } from \"@web/core/domain\";\nimport { evaluateExpr } from \"@web/core/py_js/py\";\nimport { user } from \"@web/core/user\";\nimport { sortBy, groupBy } from \"@web/core/utils/arrays\";\nimport { deepCopy } from \"@web/core/utils/objects\";\nimport { SearchArchParser } from \"./search_arch_parser\";\nimport {\n    constructDateDomain,\n    DEFAULT_INTERVAL,\n    getComparisonOptions,\n    getIntervalOptions,\n    getPeriodOptions,\n    rankInterval,\n    yearSelected,\n} from \"./utils/dates\";\nimport { FACET_ICONS, FACET_COLORS } from \"./utils/misc\";\n\nimport { EventBus, toRaw } from \"@odoo/owl\";\nimport { domainFromTree, treeFromDomain } from \"@web/core/tree_editor/condition_tree\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useGetTreeDescription, useMakeGetFieldDef } from \"@web/core/tree_editor/utils\";\nimport { DomainSelectorDialog } from \"@web/core/domain_selector_dialog/domain_selector_dialog\";\nimport { getDefaultDomain } from \"@web/core/domain_selector/utils\";\n\nconst { DateTime } = luxon;\n\n/** @typedef {import(\"@web/core/domain\").DomainRepr} DomainRepr */\n/** @typedef {import(\"@web/core/domain\").DomainListRepr} DomainListRepr */\n/** @typedef {import(\"../views/utils\").OrderTerm} OrderTerm */\n\n/**\n * @typedef {Object} ComparisonDomain\n * @property {DomainListRepr} arrayRepr\n * @property {string} description\n */\n\n/**\n * @typedef {Object} Comparison\n * @property {ComparisonDomain[]} domains\n * @property {string} [fieldName]\n */\n\n/**\n * @typedef {Object} SearchParams\n * @property {Comparison | null} comparison\n * @property {Context} context\n * @property {DomainListRepr} domain\n * @property {string[]} groupBy\n * @property {OrderTerm[]} orderBy\n * @property {boolean} [useSampleModel] to remove?\n */\n\n/** @todo rework doc */\n// interface SectionCommon { // check optional keys\n//     color: string;\n//     description: string;\n//     errorMsg: [string];\n//     enableCounters: boolean;\n//     expand: boolean;\n//     fieldName: string;\n//     icon: string;\n//     id: number;\n//     limit: number;\n//     values: Map<any,any>;\n//   }\n\n//   export interface Category extends SectionCommon {\n//     type: \"category\";\n//     hierarchize: boolean;\n//   }\n\n//   export interface Filter extends SectionCommon {\n//     type: \"filter\";\n//     domain: string;\n//     groupBy: string;\n//     groups: Map<any,any>;\n//   }\n\n//   export type Section = Category | Filter;\n\n//   export type SectionPredicate = (section: Section) => boolean;\n\n/**\n * @param {Section} section\n * @returns {boolean}\n */\nfunction hasValues(section) {\n    const { errorMsg, type, values } = section;\n    if (errorMsg) {\n        return true;\n    }\n    switch (type) {\n        case \"category\": {\n            return values && values.size > 1; // false item ignored\n        }\n        case \"filter\": {\n            return values && values.size > 0;\n        }\n    }\n}\n\n/**\n * Returns a serialised array of the given map with its values being the\n * shallow copies of the original values.\n * @param {Map<any, Object>} map\n * @return {Array[]}\n */\nfunction mapToArray(map) {\n    const result = [];\n    for (const [key, val] of map) {\n        const valCopy = Object.assign({}, val);\n        result.push([key, valCopy]);\n    }\n    return result;\n}\n/**\n * @param {Array[]}\n * @returns {Map<any, Object>} map\n */\nfunction arraytoMap(array) {\n    return new Map(array);\n}\n\n/**\n * @param {Function} op\n * @param {Object} source\n * @param {Object} target\n */\nfunction execute(op, source, target) {\n    const { query, nextId, nextGroupId, nextGroupNumber, searchItems, searchPanelInfo, sections } =\n        source;\n\n    target.nextGroupId = nextGroupId;\n    target.nextGroupNumber = nextGroupNumber;\n    target.nextId = nextId;\n\n    target.query = query;\n    target.searchItems = searchItems;\n\n    target.searchPanelInfo = searchPanelInfo;\n\n    target.sections = op(sections);\n    for (const [, section] of target.sections) {\n        section.values = op(section.values);\n        if (section.groups) {\n            section.groups = op(section.groups);\n            for (const [, group] of section.groups) {\n                group.values = op(group.values);\n            }\n        }\n    }\n}\n\n//--------------------------------------------------------------------------\n// Global constants/variables\n//--------------------------------------------------------------------------\n\nconst FAVORITE_PRIVATE_GROUP = 1;\nconst FAVORITE_SHARED_GROUP = 2;\n\nexport class SearchModel extends EventBus {\n    constructor(env, services, args) {\n        super();\n        this.env = env;\n        this.setup(services, args);\n    }\n    /**\n     * @override\n     */\n    setup(services) {\n        // services\n        const { field: fieldService, name: nameService, orm, view, dialog } = services;\n        this.orm = orm;\n        this.fieldService = fieldService;\n        this.viewService = view;\n        this.dialog = dialog;\n        this.orderByCount = false;\n\n        this.getDomainTreeDescription = useGetTreeDescription(fieldService, nameService);\n        this.makeGetFieldDef = useMakeGetFieldDef(fieldService);\n\n        // used to manage search items related to date/datetime fields\n        this.referenceMoment = DateTime.local();\n        this.comparisonOptions = getComparisonOptions();\n        this.intervalOptions = getIntervalOptions();\n    }\n\n    /**\n     *\n     * @param {Object} config\n     * @param {string} config.resModel\n     *\n     * @param {string} [config.searchViewArch=\"<search/>\"]\n     * @param {Object} [config.searchViewFields={}]\n     * @param {number|false} [config.searchViewId=false]\n     * @param {Object[]} [config.irFilters=[]]\n     *\n     * @param {boolean} [config.activateFavorite=true]\n     * @param {Object | null} [config.comparison]\n     * @param {Object} [config.context={}]\n     * @param {Array} [config.domain=[]]\n     * @param {Array} [config.dynamicFilters=[]]\n     * @param {string[]} [config.groupBy=[]]\n     * @param {boolean} [config.loadIrFilters=false]\n     * @param {boolean} [config.display.searchPanel=true]\n     * @param {OrderTerm[]} [config.orderBy=[]]\n     * @param {string[]} [config.searchMenuTypes=[\"filter\", \"groupBy\", \"favorite\"]]\n     * @param {Object} [config.state]\n     */\n    async load(config) {\n        const { resModel } = config;\n        if (!resModel) {\n            throw Error(`SearchPanel config should have a \"resModel\" key`);\n        }\n        this.resModel = resModel;\n\n        // used to avoid useless recomputations\n        this._reset();\n\n        const { comparison, context, domain, groupBy, hideCustomGroupBy, orderBy } = config;\n\n        this.globalComparison = comparison;\n        this.globalContext = toRaw(Object.assign({}, context));\n        this.globalDomain = domain || [];\n        this.globalGroupBy = groupBy || [];\n        this.globalOrderBy = orderBy || [];\n        this.hideCustomGroupBy = hideCustomGroupBy;\n\n        this.searchMenuTypes = new Set(config.searchMenuTypes || [\"filter\", \"groupBy\", \"favorite\"]);\n        this.canOrderByCount = config.canOrderByCount;\n\n        let { irFilters, loadIrFilters, searchViewArch, searchViewFields, searchViewId } = config;\n        const loadSearchView =\n            searchViewId !== undefined &&\n            (!searchViewArch || !searchViewFields || (!irFilters && loadIrFilters));\n\n        const searchViewDescription = {};\n        if (loadSearchView) {\n            const result = await this.viewService.loadViews(\n                {\n                    context: this.globalContext,\n                    resModel,\n                    views: [[searchViewId, \"search\"]],\n                },\n                {\n                    actionId: this.env.config.actionId,\n                    embeddedActionId: this.env.config.currentEmbeddedActionId,\n                    loadIrFilters: loadIrFilters || false,\n                }\n            );\n            Object.assign(searchViewDescription, result.views.search);\n            searchViewFields = searchViewFields || result.fields;\n        }\n        if (searchViewArch) {\n            searchViewDescription.arch = searchViewArch;\n        }\n        if (irFilters) {\n            searchViewDescription.irFilters = irFilters;\n        }\n        if (searchViewId !== undefined) {\n            searchViewDescription.viewId = searchViewId;\n        }\n        this.searchViewArch = searchViewDescription.arch || \"<search/>\";\n        this.searchViewFields = searchViewFields || {};\n        if (searchViewDescription.irFilters) {\n            this.irFilters = searchViewDescription.irFilters;\n        }\n        if (searchViewDescription.viewId !== undefined) {\n            this.searchViewId = searchViewDescription.viewId;\n        }\n\n        const { searchDefaults, searchPanelDefaults } =\n            this._extractSearchDefaultsFromGlobalContext();\n\n        if (config.state) {\n            this._importState(config.state);\n            this.__legacyParseSearchPanelArchAnyway(searchViewDescription, searchViewFields);\n            this.display = this._getDisplay(config.display);\n            if (!this.searchPanelInfo.loaded) {\n                return this._reloadSections();\n            }\n            return;\n        }\n\n        this.blockNotification = true;\n\n        this.searchItems = {};\n        this.query = [];\n\n        this.nextId = 1;\n        this.nextGroupId = 1;\n        this.nextGroupNumber = 1;\n\n        const parser = new SearchArchParser(\n            searchViewDescription,\n            searchViewFields,\n            searchDefaults,\n            searchPanelDefaults\n        );\n        const { labels, preSearchItems, searchPanelInfo, sections } = parser.parse();\n\n        this.searchPanelInfo = { ...searchPanelInfo, loaded: false, shouldReload: false };\n\n        await Promise.all(labels.map((cb) => cb(this.orm)));\n\n        // prepare search items (populate this.searchItems)\n        for (const preGroup of preSearchItems || []) {\n            this._createGroupOfSearchItems(preGroup);\n        }\n        this.nextGroupNumber =\n            1 + Math.max(...Object.values(this.searchItems).map((i) => i.groupNumber || 0), 0);\n\n        const dateFilters = Object.values(this.searchItems).filter(\n            (searchElement) => searchElement.type === \"dateFilter\"\n        );\n        if (dateFilters.length) {\n            this._createGroupOfComparisons(dateFilters);\n        }\n\n        const { dynamicFilters } = config;\n        if (dynamicFilters) {\n            this._createGroupOfDynamicFilters(dynamicFilters);\n        }\n\n        const defaultFavoriteId = this._createGroupOfFavorites(this.irFilters || []);\n        const activateFavorite = \"activateFavorite\" in config ? config.activateFavorite : true;\n\n        // activate default search items (populate this.query)\n        this._activateDefaultSearchItems(activateFavorite ? defaultFavoriteId : null);\n\n        // prepare search panel sections\n\n        /** @type Map<number,Section> */\n        this.sections = new Map(sections || []);\n        this.display = this._getDisplay(config.display);\n\n        if (this.display.searchPanel) {\n            /** @type DomainListRepr */\n            this.searchDomain = this._getDomain({ withSearchPanel: false });\n            this.sectionsPromise = this._fetchSections(this.categories, this.filters).then(() => {\n                for (const { fieldName, values } of this.filters) {\n                    const filterDefaults = searchPanelDefaults[fieldName] || [];\n                    for (const valueId of filterDefaults) {\n                        const value = values.get(valueId);\n                        if (value) {\n                            value.checked = true;\n                        }\n                    }\n                }\n            });\n            if (Object.keys(searchPanelDefaults).length || this._shouldWaitForData(false)) {\n                await this.sectionsPromise;\n            }\n        }\n\n        this.blockNotification = false;\n    }\n\n    /**\n     * @param {Object} [config={}]\n     * @param {Object | null} [config.comparison]\n     * @param {Object} [config.context={}]\n     * @param {Array} [config.domain=[]]\n     * @param {string[]} [config.groupBy=[]]\n     * @param {OrderTerm[]} [config.orderBy=[]]\n     */\n    async reload(config = {}) {\n        this._reset();\n\n        const { comparison, context, domain, groupBy, orderBy } = config;\n\n        this.globalContext = Object.assign({}, context);\n        this.globalDomain = domain || [];\n        this.globalComparison = comparison;\n        this.globalGroupBy = groupBy || [];\n        this.globalOrderBy = orderBy || [];\n\n        this._extractSearchDefaultsFromGlobalContext();\n\n        await this._reloadSections();\n    }\n\n    //--------------------------------------------------------------------------\n    // Getters\n    //--------------------------------------------------------------------------\n\n    /**\n     * @returns {Category[]}\n     */\n    get categories() {\n        return [...this.sections.values()].filter((s) => s.type === \"category\");\n    }\n\n    /**\n     * @returns {Context} should be imported from context.js?\n     */\n    get context() {\n        if (!this._context) {\n            this._context = makeContext([this.globalContext, this._getContext()]);\n        }\n        return deepCopy(this._context);\n    }\n\n    /**\n     * @returns {DomainListRepr}\n     */\n    get domain() {\n        if (!this._domain) {\n            this._domain = this._getDomain();\n        }\n        return deepCopy(this._domain);\n    }\n\n    /**\n     * @returns {string}\n     */\n    get domainString() {\n        return this._getDomain({ raw: true }).toString();\n    }\n\n    get domainEvalContext() {\n        return Object.assign({}, this.globalContext, user.context);\n    }\n\n    /**\n     * @returns {Comparison}\n     */\n    get comparison() {\n        if (!this.searchMenuTypes.has(\"comparison\")) {\n            return null;\n        }\n        if (this._comparison === undefined) {\n            if (this.globalComparison) {\n                this._comparison = this.globalComparison;\n            } else {\n                const comparison = this.getFullComparison();\n                if (comparison) {\n                    const {\n                        fieldName,\n                        range,\n                        rangeDescription,\n                        comparisonRange,\n                        comparisonRangeDescription,\n                    } = comparison;\n                    const domains = [\n                        {\n                            arrayRepr: Domain.and([this.domain, range]).toList(),\n                            description: rangeDescription,\n                        },\n                        {\n                            arrayRepr: Domain.and([this.domain, comparisonRange]).toList(),\n                            description: comparisonRangeDescription,\n                        },\n                    ];\n                    this._comparison = { domains, fieldName };\n                } else {\n                    this._comparison = null;\n                }\n            }\n        }\n        return deepCopy(this._comparison);\n    }\n\n    get facets() {\n        const isValidType = (type) =>\n            ![\"groupBy\", \"comparison\"].includes(type) || this.searchMenuTypes.has(type);\n        const facets = [];\n        for (const facet of this._getFacets()) {\n            if (!isValidType(facet.type)) {\n                continue;\n            }\n            facets.push(facet);\n        }\n        return facets;\n    }\n\n    /**\n     * @returns {Filter[]}\n     */\n    get filters() {\n        return [...this.sections.values()].filter((s) => s.type === \"filter\");\n    }\n\n    /**\n     * @returns {string[]}\n     */\n    get groupBy() {\n        if (!this.searchMenuTypes.has(\"groupBy\")) {\n            return [];\n        }\n        if (!this._groupBy) {\n            this._groupBy = this._getGroupBy();\n        }\n        return deepCopy(this._groupBy);\n    }\n\n    /**\n     * @returns {OrderTerm[]}\n     */\n    get orderBy() {\n        if (!this._orderBy) {\n            this._orderBy = this._getOrderBy();\n        }\n        return deepCopy(this._orderBy);\n    }\n\n    get isDebugMode() {\n        return !!this.env.debug;\n    }\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * Activate a filter of type 'field' with given filterId with\n     * 'autocompleteValues' value, label, and operator.\n     * @param {Object}\n     */\n    addAutoCompletionValues(searchItemId, autocompleteValue) {\n        const searchItem = this.searchItems[searchItemId];\n        if (![\"field\", \"field_property\"].includes(searchItem.type)) {\n            return;\n        }\n        const { label, value, operator } = autocompleteValue;\n        const queryElem = this.query.find(\n            (queryElem) =>\n                queryElem.searchItemId === searchItemId &&\n                \"autocompleteValue\" in queryElem &&\n                queryElem.autocompleteValue.value === value &&\n                queryElem.autocompleteValue.operator === operator\n        );\n        if (!queryElem) {\n            this.query.push({ searchItemId, autocompleteValue });\n        } else {\n            queryElem.autocompleteValue.label = label; // seems related to old stuff --> should be useless now\n        }\n        this._notify();\n    }\n\n    /**\n     * Remove all the query elements from query.\n     */\n    clearQuery() {\n        this.query = [];\n        this.orderByCount = false;\n        this._notify();\n    }\n\n    /**\n     * Create a new filter of type 'favorite' and activate it.\n     * A new group containing only that filter is created.\n     * The query is emptied before activating the new favorite.\n     * @param {Object} params\n     * @returns {Promise}\n     */\n    async createNewFavorite(params) {\n        const { preFavorite, irFilter } = this._getIrFilterDescription(params);\n        const serverSideId = await this._createIrFilters(irFilter);\n\n        // before the filter cache was cleared!\n        this.blockNotification = true;\n        this.clearQuery();\n        const favorite = {\n            ...preFavorite,\n            type: \"favorite\",\n            id: this.nextId,\n            groupId: this.nextGroupId,\n            groupNumber: preFavorite.userId ? FAVORITE_PRIVATE_GROUP : FAVORITE_SHARED_GROUP,\n            removable: true,\n            serverSideId,\n        };\n        this.searchItems[this.nextId] = favorite;\n        this.query.push({ searchItemId: this.nextId });\n        this.nextGroupId++;\n        this.nextId++;\n        this.blockNotification = false;\n        this._notify();\n    }\n\n    async _createIrFilters(irFilter) {\n        const serverSideId = await this.orm.call(\"ir.filters\", \"create_or_replace\", [irFilter]);\n        this.env.bus.trigger(\"CLEAR-CACHES\");\n        return serverSideId;\n    }\n\n    /**\n     * Create new search items of type 'filter' and activate them.\n     * A new group containing only those filters is created.\n     */\n    createNewFilters(prefilters) {\n        if (!prefilters.length) {\n            return [];\n        }\n        prefilters.forEach((preFilter) => {\n            const filter = Object.assign(preFilter, {\n                groupId: this.nextGroupId,\n                groupNumber: this.nextGroupNumber,\n                id: this.nextId,\n                type: \"filter\",\n            });\n            this.searchItems[this.nextId] = filter;\n            this.query.push({ searchItemId: this.nextId });\n            this.nextId++;\n        });\n        this.nextGroupId++;\n        this.nextGroupNumber++;\n        this._notify();\n    }\n\n    /**\n     * Create a new filter of type 'groupBy' or 'dateGroupBy' and activate it.\n     * It is added to the unique group of groupbys.\n     * @param {string} fieldName\n     * @param {Object} [param]\n     * @param {string} [param.interval=DEFAULT_INTERVAL]\n     * @param {boolean} [param.invisible=false]\n     */\n    createNewGroupBy(fieldName, { interval, invisible } = {}) {\n        const field = this.searchViewFields[fieldName];\n        const { string, type: fieldType } = field;\n        const firstGroupBy = Object.values(this.searchItems).find((f) => f.type === \"groupBy\");\n        const preSearchItem = {\n            description: string || fieldName,\n            fieldName,\n            fieldType,\n            groupId: firstGroupBy ? firstGroupBy.groupId : this.nextGroupId++,\n            groupNumber: this.nextGroupNumber,\n            id: this.nextId,\n            custom: true,\n        };\n        if (invisible) {\n            preSearchItem.invisible = \"True\";\n        }\n        if ([\"date\", \"datetime\"].includes(fieldType)) {\n            this.searchItems[this.nextId] = Object.assign(\n                { type: \"dateGroupBy\", defaultIntervalId: interval || DEFAULT_INTERVAL },\n                preSearchItem\n            );\n            this.toggleDateGroupBy(this.nextId);\n        } else {\n            this.searchItems[this.nextId] = Object.assign({ type: \"groupBy\" }, preSearchItem);\n            this.toggleSearchItem(this.nextId);\n        }\n        this.nextGroupNumber++; // FIXME: with this, all subsequent added groups are in different groups (visually)\n        this.nextId++;\n        this._notify();\n    }\n\n    /**\n     * Deactivate a group with provided groupId, i.e. delete the query elements\n     * with given groupId.\n     */\n    deactivateGroup(groupId) {\n        this.query = this.query.filter((queryElem) => {\n            const searchItem = this.searchItems[queryElem.searchItemId];\n            return searchItem.groupId !== groupId;\n        });\n        this._checkComparisonStatus();\n        this._checkOrderByCountStatus();\n        this._notify();\n    }\n\n    /**\n     * Delete a filter of type 'favorite' with given this.nextId server side and\n     * in control panel model. Of course the filter is also removed\n     * from the search query.\n     */\n    async deleteFavorite(favoriteId) {\n        const searchItem = this.searchItems[favoriteId];\n        if (searchItem.type !== \"favorite\") {\n            return;\n        }\n        await this._deleteIrFilters(searchItem);\n        const index = this.query.findIndex((queryElem) => queryElem.searchItemId === favoriteId);\n        delete this.searchItems[favoriteId];\n        if (index >= 0) {\n            this.query.splice(index, 1);\n        }\n        this._notify();\n    }\n\n    async _deleteIrFilters(searchItem) {\n        const { serverSideId } = searchItem;\n        await this.orm.unlink(\"ir.filters\", [serverSideId]);\n        this.env.bus.trigger(\"CLEAR-CACHES\");\n    }\n\n    /**\n     * @returns {Object}\n     */\n    exportState() {\n        const state = {};\n        execute(mapToArray, this, state);\n        return state;\n    }\n\n    getFullComparison() {\n        let searchItem = null;\n        for (const queryElem of this.query.slice().reverse()) {\n            const item = this.searchItems[queryElem.searchItemId];\n            if (item.type === \"comparison\") {\n                searchItem = item;\n                break;\n            } else if (item.type === \"favorite\" && item.comparison) {\n                searchItem = item;\n                break;\n            }\n        }\n        if (!searchItem) {\n            return null;\n        } else if (searchItem.type === \"favorite\") {\n            return searchItem.comparison;\n        }\n        const { dateFilterId, comparisonOptionId } = searchItem;\n        const dateFilter = this.searchItems[dateFilterId];\n        const { fieldName, description: dateFilterDescription } = dateFilter;\n        const selectedGeneratorIds = this._getSelectedGeneratorIds(dateFilterId);\n        // compute range and range description\n        const { domain: range, description: rangeDescription } = constructDateDomain(\n            this.referenceMoment,\n            dateFilter,\n            selectedGeneratorIds\n        );\n        // compute comparisonRange and comparisonRange description\n        const { domain: comparisonRange, description: comparisonRangeDescription } =\n            constructDateDomain(\n                this.referenceMoment,\n                dateFilter,\n                selectedGeneratorIds,\n                comparisonOptionId\n            );\n        return {\n            comparisonId: comparisonOptionId,\n            fieldName,\n            fieldDescription: dateFilterDescription,\n            range: range.toList(),\n            rangeDescription,\n            comparisonRange: comparisonRange.toList(),\n            comparisonRangeDescription,\n        };\n    }\n\n    getIrFilterValues(params) {\n        const { irFilter } = this._getIrFilterDescription(params);\n        return irFilter;\n    }\n\n    getPreFavoriteValues(params) {\n        const { preFavorite } = this._getIrFilterDescription(params);\n        return preFavorite;\n    }\n\n    /**\n     * Return an array containing enriched copies of all searchElements or of those\n     * satifying the given predicate if any\n     * @param {Function} [predicate]\n     * @returns {Object[]}\n     */\n    getSearchItems(predicate) {\n        const searchItems = [];\n        for (const searchItem of Object.values(this.searchItems)) {\n            const enrichedSearchitem = this._enrichItem(searchItem);\n            if (enrichedSearchitem) {\n                const isInvisible =\n                    \"invisible\" in searchItem &&\n                    evaluateExpr(searchItem.invisible, this.globalContext);\n                if (!isInvisible && (!predicate || predicate(enrichedSearchitem))) {\n                    searchItems.push(enrichedSearchitem);\n                }\n            }\n        }\n        if (searchItems.some((f) => f.type === \"favorite\")) {\n            searchItems.sort((f1, f2) => f1.groupNumber - f2.groupNumber);\n        }\n        return searchItems;\n    }\n\n    /**\n     * Returns a sorted list of a copy of all sections. This list can be\n     * filtered by a given predicate.\n     * @param {SectionPredicate} [predicate] used to determine\n     *      which subsets of sections is wanted\n     * @returns {Section[]}\n     */\n    getSections(predicate) {\n        let sections = [...this.sections.values()].map((section) =>\n            Object.assign({}, section, { empty: !hasValues(section) })\n        );\n        if (predicate) {\n            sections = sections.filter(predicate);\n        }\n        return sections.sort((s1, s2) => s1.index - s2.index);\n    }\n\n    search() {\n        this.trigger(\"update\");\n    }\n\n    async splitAndAddDomain(domain, groupId) {\n        const group = groupId ? this._getGroups().find((g) => g.id === groupId) : null;\n        let context;\n        if (group) {\n            const contexts = [];\n            for (const activeItem of group.activeItems) {\n                const context = this._getSearchItemContext(activeItem);\n                if (context) {\n                    contexts.push(context);\n                }\n            }\n            context = makeContext(contexts);\n        }\n\n        const getFieldDef = await this.makeGetFieldDef(this.resModel, treeFromDomain(domain));\n        const tree = treeFromDomain(domain, { distributeNot: !this.isDebugMode, getFieldDef });\n        const trees = !tree.negate && tree.value === \"&\" ? tree.children : [tree];\n        const promises = trees.map(async (tree) => {\n            const description = await this.getDomainTreeDescription(this.resModel, tree);\n            const preFilter = {\n                description,\n                domain: domainFromTree(tree),\n                invisible: \"True\",\n                type: \"filter\",\n            };\n            if (context) {\n                preFilter.context = context;\n            }\n            return preFilter;\n        });\n\n        const preFilters = await Promise.all(promises);\n\n        this.blockNotification = true;\n\n        if (group) {\n            const firstActiveItem = group.activeItems[0];\n            const firstSearchItem = this.searchItems[firstActiveItem.searchItemId];\n            const { type } = firstSearchItem;\n            if (type === \"favorite\") {\n                const activeItemGroupBys = this._getSearchItemGroupBys(firstActiveItem);\n                for (const activeItemGroupBy of activeItemGroupBys) {\n                    const [fieldName, interval] = activeItemGroupBy.split(\":\");\n                    this.createNewGroupBy(fieldName, { interval, invisible: true });\n                }\n                const index = this.query.length - activeItemGroupBys.length;\n                this.query = [...this.query.slice(index), ...this.query.slice(0, index)];\n            }\n            this.deactivateGroup(groupId);\n        }\n\n        for (const preFilter of preFilters) {\n            this.createNewFilters([preFilter]);\n        }\n\n        this.blockNotification = false;\n\n        this._notify();\n    }\n\n    /**\n     * Set the active value id of a given category.\n     * @param {number} sectionId\n     * @param {number} valueId\n     */\n    toggleCategoryValue(sectionId, valueId) {\n        const category = this.sections.get(sectionId);\n        category.activeValueId = valueId;\n        this._notify();\n    }\n\n    /**\n     * Toggle a filter value of a given section. The value will be set\n     * to \"forceTo\" if provided, else it will be its own opposed value.\n     * @param {number} sectionId\n     * @param {number[]} valueIds\n     * @param {boolean} [forceTo=null]\n     */\n    toggleFilterValues(sectionId, valueIds, forceTo = null) {\n        const filter = this.sections.get(sectionId);\n        for (const valueId of valueIds) {\n            const value = filter.values.get(valueId);\n            value.checked = forceTo === null ? !value.checked : forceTo;\n        }\n        this._notify();\n    }\n\n    /**\n     * Clears all values from the provided sections\n     * @param {array} sectionIds\n     */\n    clearSections(sectionIds) {\n        for (const sectionId of sectionIds) {\n            const section = this.sections.get(sectionId);\n            if (section.type === \"category\") {\n                section.activeValueId = false;\n            } else {\n                for (const [, value] of section.values) {\n                    value.checked = false;\n                }\n            }\n        }\n        this._notify();\n    }\n\n    /**\n     * Activate or deactivate the simple filter with given filterId, i.e.\n     * add or remove a corresponding query element.\n     */\n    toggleSearchItem(searchItemId) {\n        const searchItem = this.searchItems[searchItemId];\n        switch (searchItem.type) {\n            case \"dateFilter\":\n            case \"dateGroupBy\":\n            case \"field_property\":\n            case \"field\": {\n                return;\n            }\n        }\n        const index = this.query.findIndex((queryElem) => queryElem.searchItemId === searchItemId);\n        if (index >= 0) {\n            this.query.splice(index, 1);\n            this._checkOrderByCountStatus();\n        } else {\n            if (searchItem.type === \"favorite\") {\n                this.query = [];\n            } else if (searchItem.type === \"comparison\") {\n                // make sure only one comparison can be active\n                this.query = this.query.filter((queryElem) => {\n                    const { type } = this.searchItems[queryElem.searchItemId];\n                    return type !== \"comparison\";\n                });\n            }\n            this.query.push({ searchItemId });\n        }\n        this._notify();\n    }\n\n    /**\n     * Used to toggle a query element.\n     * This can impact the query in various form, e.g. add/remove other query elements\n     * in case the filter is of type 'filter'.\n     */\n    toggleDateFilter(searchItemId, generatorId) {\n        const searchItem = this.searchItems[searchItemId];\n        if (searchItem.type !== \"dateFilter\") {\n            return;\n        }\n        const generatorIds = generatorId ? [generatorId] : searchItem.defaultGeneratorIds;\n        for (const generatorId of generatorIds) {\n            const index = this.query.findIndex(\n                (queryElem) =>\n                    queryElem.searchItemId === searchItemId &&\n                    \"generatorId\" in queryElem &&\n                    queryElem.generatorId === generatorId\n            );\n            if (index >= 0) {\n                this.query.splice(index, 1);\n                if (!yearSelected(this._getSelectedGeneratorIds(searchItemId))) {\n                    // This is the case where generatorId was the last option\n                    // of type 'year' to be there before being removed above.\n                    // Since other options of type 'month' or 'quarter' do\n                    // not make sense without a year we deactivate all options.\n                    this.query = this.query.filter(\n                        (queryElem) => queryElem.searchItemId !== searchItemId\n                    );\n                }\n            } else {\n                if (generatorId.startsWith(\"custom\")) {\n                    const comparisonId = this._getActiveComparison()?.id;\n                    this.query = this.query.filter(\n                        (queryElem) =>\n                            ![searchItemId, comparisonId].includes(queryElem.searchItemId)\n                    );\n                    this.query.push({ searchItemId, generatorId });\n                    continue;\n                }\n                this.query = this.query.filter(\n                    (queryElem) =>\n                        queryElem.searchItemId !== searchItemId ||\n                        !queryElem.generatorId.startsWith(\"custom\")\n                );\n                this.query.push({ searchItemId, generatorId });\n                if (!yearSelected(this._getSelectedGeneratorIds(searchItemId))) {\n                    // Here we add 'year' as options if no option of type\n                    // year is already selected.\n                    const { defaultYearId } = getPeriodOptions(\n                        this.referenceMoment,\n                        searchItem.optionsParams\n                    ).find((o) => o.id === generatorId);\n                    this.query.push({ searchItemId, generatorId: defaultYearId });\n                }\n            }\n        }\n        this._checkComparisonStatus();\n        this._notify();\n    }\n\n    toggleDateGroupBy(searchItemId, intervalId) {\n        const searchItem = this.searchItems[searchItemId];\n        if (searchItem.type !== \"dateGroupBy\") {\n            return;\n        }\n        intervalId = intervalId || searchItem.defaultIntervalId;\n        const index = this.query.findIndex(\n            (queryElem) =>\n                queryElem.searchItemId === searchItemId &&\n                \"intervalId\" in queryElem &&\n                queryElem.intervalId === intervalId\n        );\n        if (index >= 0) {\n            this.query.splice(index, 1);\n            this._checkOrderByCountStatus();\n        } else {\n            this.query.push({ searchItemId, intervalId });\n        }\n        this._notify();\n    }\n\n    async spawnCustomFilterDialog() {\n        const domain = getDefaultDomain(this.searchViewFields);\n        this.dialog.add(DomainSelectorDialog, {\n            resModel: this.resModel,\n            defaultConnector: \"|\",\n            domain,\n            context: this.domainEvalContext,\n            onConfirm: (domain) => this.splitAndAddDomain(domain),\n            disableConfirmButton: (domain) => domain === `[]`,\n            title: _t(\"Add Custom Filter\"),\n            confirmButtonText: _t(\"Add\"),\n            discardButtonText: _t(\"Cancel\"),\n            isDebugMode: this.isDebugMode,\n        });\n    }\n\n    switchGroupBySort() {\n        if (this.orderByCount === \"Desc\") {\n            this.orderByCount = \"Asc\";\n        } else {\n            this.orderByCount = \"Desc\";\n        }\n        this._notify();\n    }\n\n    /**\n     * Generate the searchItems corresponding to the properties.\n     * @param {Object} searchItem\n     * @returns {Object[]}\n     */\n    async getSearchItemsProperties(searchItem) {\n        if (searchItem.type !== \"field\" || searchItem.fieldType !== \"properties\") {\n            return [];\n        }\n        const field = this.searchViewFields[searchItem.fieldName];\n        const definitionRecord = field.definition_record;\n        const result = await this._fetchPropertiesDefinition(this.resModel, searchItem.fieldName);\n\n        const searchItemIds = new Set();\n        const existingFieldProperties = {};\n        for (const item of Object.values(this.searchItems)) {\n            if (item.type === \"field_property\" && item.propertyItemId === searchItem.id) {\n                existingFieldProperties[item.propertyFieldDefinition.name] = item;\n            }\n        }\n\n        for (const { definitionRecordId, definitionRecordName, definitions } of result) {\n            for (const definition of definitions) {\n                if (definition.type === \"separator\") {\n                    continue;\n                }\n                const existingSearchItem = existingFieldProperties[definition.name];\n                if (existingSearchItem) {\n                    // already in the list, can happen if we unfold the properties field\n                    // open a form view, edit the property and then go back to the search view\n                    // the label of the property might have been changed\n                    existingSearchItem.description = `${definition.string} (${definitionRecordName})`;\n                    searchItemIds.add(existingSearchItem.id);\n                    continue;\n                }\n                const id = this.nextId++;\n                const newSearchItem = {\n                    id,\n                    type: \"field_property\",\n                    fieldName: searchItem.fieldName,\n                    propertyDomain: [definitionRecord, \"=\", definitionRecordId],\n                    propertyFieldDefinition: definition,\n                    propertyItemId: searchItem.id,\n                    description: `${definition.string} (${definitionRecordName})`,\n                    groupId: this.nextGroupId++,\n                };\n                if ([\"many2many\", \"tags\"].includes(definition.type)) {\n                    newSearchItem.operator = \"in\";\n                }\n                this.searchItems[id] = newSearchItem;\n                searchItemIds.add(id);\n            }\n        }\n\n        return this.getSearchItems((searchItem) => searchItemIds.has(searchItem.id));\n    }\n\n    //--------------------------------------------------------------------------\n    // Private methods\n    //--------------------------------------------------------------------------\n\n    /**\n     * Because it require a RPC to get the properties search views items,\n     * it's done lazily, only when we need them.\n     */\n    async fillSearchViewItemsProperty() {\n        if (!this.searchViewFields) {\n            return;\n        }\n\n        const fields = Object.values(this.searchViewFields);\n\n        for (const field of fields) {\n            if (field.type !== \"properties\") {\n                continue;\n            }\n\n            const result = await this._fetchPropertiesDefinition(this.resModel, field.name);\n\n            const searchItemsNames = Object.values(this.searchItems)\n                .filter((item) => item.isProperty && [\"groupBy\", \"dateGroupBy\"].includes(item.type))\n                .map((item) => item.fieldName);\n\n            for (const { definitionRecordId, definitionRecordName, definitions } of result) {\n                // some properties might have been deleted\n                const groupNames = definitions.map(\n                    (definition) => `group_by_${field.name}.${definition.name}`\n                );\n                Object.values(this.searchItems).forEach((searchItem) => {\n                    if (\n                        searchItem.isProperty &&\n                        searchItem.definitionRecordId === definitionRecordId &&\n                        [\"groupBy\", \"dateGroupBy\"].includes(searchItem.type) &&\n                        !groupNames.includes(searchItem.name)\n                    ) {\n                        // we can not just remove the element from the list because index are used as id\n                        // so we use a different type to hide it everywhere (until the user refresh his\n                        // browser and the item won't be created again)\n                        searchItem.type = \"group_by_property_deleted\";\n                    }\n                });\n\n                for (const definition of definitions) {\n                    // we need the definition of the \"field\" (fake field, property) to be\n                    // in searchViewFields to be able to have the type, it's description, etc\n                    // the name of the property is stored as \"<properties field name>.<property name>\"\n                    const fullName = `${field.name}.${definition.name}`;\n                    this.searchViewFields[fullName] = {\n                        name: fullName,\n                        readonly: false,\n                        relation: definition.comodel,\n                        required: false,\n                        searchable: false,\n                        selection: definition.selection,\n                        sortable: true,\n                        store: true,\n                        string: `${definition.string} (${definitionRecordName})`,\n                        type: definition.type,\n                        relatedPropertyField: field,\n                    };\n\n                    if (!searchItemsNames.includes(fullName)) {\n                        const groupByItem = {\n                            description: definition.string,\n                            definitionRecordId,\n                            definitionRecordName,\n                            fieldName: fullName,\n                            fieldType: definition.type,\n                            isProperty: true,\n                            name: `group_by_${field.name}.${definition.name}`,\n                            propertyFieldName: field.name,\n                            type: [\"datetime\", \"date\"].includes(definition.type)\n                                ? \"dateGroupBy\"\n                                : \"groupBy\",\n                        };\n                        this._createGroupOfSearchItems([groupByItem]);\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Fetch the properties definitions.\n     *\n     * @param {string} definitionRecordModel\n     * @param {string} definitionRecordField\n     * @return {Object[]} A list of objects of the form\n     *      {\n     *          definitionRecordId: <id of the parent record>\n     *          definitionRecordName: <display name of the parent record>\n     *          definitions: <list of properties definitions>\n     *      }\n     */\n    async _fetchPropertiesDefinition(resModel, fieldName) {\n        const domain = [];\n        if (this.context.active_id) {\n            // assume the active id is the definition record\n            // and show only its properties\n            domain.push([\"id\", \"=\", this.context.active_id]);\n        }\n\n        const definitions = await this.fieldService.loadPropertyDefinitions(\n            resModel,\n            fieldName,\n            domain\n        );\n        const result = groupBy(Object.values(definitions), (definition) => definition.record_id);\n        return Object.entries(result).map(([recordId, definitions]) => {\n            return {\n                definitionRecordId: parseInt(recordId),\n                definitionRecordName: definitions[0]?.record_name,\n                definitions,\n            };\n        });\n    }\n\n    /**\n     * Activate the default favorite (if any) or all default filters.\n     */\n    _activateDefaultSearchItems(defaultFavoriteId) {\n        if (defaultFavoriteId) {\n            // Activate default favorite\n            this.toggleSearchItem(defaultFavoriteId);\n        } else {\n            // Activate default filters\n            Object.values(this.searchItems)\n                .filter((f) => f.isDefault && f.type !== \"favorite\")\n                .sort((f1, f2) => (f1.defaultRank || 100) - (f2.defaultRank || 100))\n                .forEach((f) => {\n                    if (f.type === \"dateFilter\") {\n                        this.toggleDateFilter(f.id);\n                    } else if (f.type === \"dateGroupBy\") {\n                        this.toggleDateGroupBy(f.id);\n                    } else if (f.type === \"field\") {\n                        this.addAutoCompletionValues(f.id, f.defaultAutocompleteValue);\n                    } else {\n                        this.toggleSearchItem(f.id);\n                    }\n                });\n        }\n    }\n\n    /**\n     * If a comparison is active, check if it should become inactive.\n     * The comparison should become inactive if the corresponding date filter has become\n     * inactive.\n     */\n    _checkComparisonStatus() {\n        const activeComparison = this._getActiveComparison();\n        if (!activeComparison) {\n            return;\n        }\n        const { dateFilterId, id } = activeComparison;\n        const dateFilterIsActive = this.query.some(\n            (queryElem) => queryElem.searchItemId === dateFilterId\n        );\n        if (!dateFilterIsActive) {\n            this.query = this.query.filter((queryElem) => queryElem.searchItemId !== id);\n        }\n    }\n\n    _checkOrderByCountStatus() {\n        if (\n            this.orderByCount &&\n            !this.query.some((item) =>\n                [\"dateGroupBy\", \"groupBy\"].includes(this.searchItems[item.searchItemId].type)\n            )\n        ) {\n            this.orderByCount = false;\n        }\n    }\n\n    /**\n     * @param {string} sectionId\n     * @param {Object} result\n     */\n    _createCategoryTree(sectionId, result) {\n        const category = this.sections.get(sectionId);\n\n        let { error_msg, parent_field: parentField, values } = result;\n        if (error_msg) {\n            category.errorMsg = error_msg;\n            values = [];\n        }\n        if (category.hierarchize) {\n            category.parentField = parentField;\n        }\n        for (const value of values) {\n            category.values.set(\n                value.id,\n                Object.assign({}, value, {\n                    childrenIds: [],\n                    parentId: value[parentField] || false,\n                })\n            );\n        }\n        for (const value of values) {\n            const { parentId } = category.values.get(value.id);\n            if (parentId && category.values.has(parentId)) {\n                category.values.get(parentId).childrenIds.push(value.id);\n            }\n        }\n        // collect rootIds\n        category.rootIds = [false];\n        for (const value of values) {\n            const { parentId } = category.values.get(value.id);\n            if (!parentId) {\n                category.rootIds.push(value.id);\n            }\n        }\n        // Set active value from context\n        const valueIds = [false, ...values.map((val) => val.id)];\n        this._ensureCategoryValue(category, valueIds);\n    }\n\n    /**\n     * @param {string} sectionId\n     * @param {Object} result\n     */\n    _createFilterTree(sectionId, result) {\n        const filter = this.sections.get(sectionId);\n\n        let { error_msg, values } = result;\n        if (error_msg) {\n            filter.errorMsg = error_msg;\n            values = [];\n        }\n\n        // restore checked property\n        values.forEach((value) => {\n            const oldValue = filter.values.get(value.id);\n            value.checked = oldValue ? oldValue.checked : false;\n        });\n\n        filter.values = new Map();\n        const groupIds = [];\n        if (filter.groupBy) {\n            const groups = new Map();\n            for (const value of values) {\n                const groupId = value.group_id;\n                if (!groups.has(groupId)) {\n                    if (groupId) {\n                        groupIds.push(groupId);\n                    }\n                    groups.set(groupId, {\n                        id: groupId,\n                        name: value.group_name,\n                        values: new Map(),\n                        tooltip: value.group_tooltip,\n                        sequence: value.group_sequence,\n                        color_index: value.color_index,\n                    });\n                    // restore former checked state\n                    const oldGroup = filter.groups && filter.groups.get(groupId);\n                    groups.get(groupId).state = (oldGroup && oldGroup.state) || false;\n                }\n                groups.get(groupId).values.set(value.id, value);\n            }\n            filter.groups = groups;\n            filter.sortedGroupIds = sortBy(\n                groupIds,\n                (id) => groups.get(id).sequence || groups.get(id).name\n            );\n            for (const group of filter.groups.values()) {\n                for (const [valueId, value] of group.values) {\n                    filter.values.set(valueId, value);\n                }\n            }\n        } else {\n            for (const value of values) {\n                filter.values.set(value.id, value);\n            }\n        }\n    }\n\n    /**\n     * Starting from the array of date filters, create the filters of type\n     * 'comparison'.\n     * @param {Object[]} dateFilters\n     */\n    _createGroupOfComparisons(dateFilters) {\n        const preSearchItem = [];\n        for (const dateFilter of dateFilters) {\n            for (const comparisonOption of this.comparisonOptions) {\n                const { id: dateFilterId, description } = dateFilter;\n                const preFilter = {\n                    type: \"comparison\",\n                    comparisonOptionId: comparisonOption.id,\n                    description: `${description}: ${comparisonOption.description}`,\n                    dateFilterId,\n                };\n                preSearchItem.push(preFilter);\n            }\n        }\n        this._createGroupOfSearchItems(preSearchItem);\n    }\n\n    /**\n     * Add filters of type 'filter' determined by the key array dynamicFilters.\n     */\n    _createGroupOfDynamicFilters(dynamicFilters) {\n        const pregroup = dynamicFilters.map((filter) => {\n            return {\n                groupNumber: this.nextGroupNumber,\n                description: filter.description,\n                domain: filter.domain,\n                isDefault: \"is_default\" in filter ? filter.is_default : true,\n                type: \"filter\",\n            };\n        });\n        this.nextGroupNumber++;\n        this._createGroupOfSearchItems(pregroup);\n    }\n\n    /**\n     * Add filters of type 'favorite' determined by the array this.favoriteFilters.\n     */\n    _createGroupOfFavorites(irFilters) {\n        let defaultFavoriteId = null;\n        irFilters.forEach((irFilter) => {\n            const favorite = this._irFilterToFavorite(irFilter);\n            this._createGroupOfSearchItems([favorite]);\n            if (favorite.isDefault) {\n                defaultFavoriteId = favorite.id;\n            }\n        });\n        return defaultFavoriteId;\n    }\n\n    /**\n     * Using a list (a 'pregroup') of 'prefilters', create new filters in `searchItems`\n     * for each prefilter. The new filters belong to a same new group.\n     */\n    _createGroupOfSearchItems(pregroup) {\n        pregroup.forEach((preSearchItem) => {\n            const searchItem = Object.assign(preSearchItem, {\n                groupId: this.nextGroupId,\n                id: this.nextId,\n            });\n            this.searchItems[this.nextId] = searchItem;\n            this.nextId++;\n        });\n        this.nextGroupId++;\n    }\n\n    /**\n     * Returns null or a copy of the provided filter with additional information\n     * used only outside of the control panel model, like in search bar or in the\n     * various menus. The value null is returned if the filter should not appear\n     * for some reason.\n     */\n    _enrichItem(searchItem) {\n        if (searchItem.type === \"field\" && searchItem.fieldType === \"properties\") {\n            return { ...searchItem };\n        }\n        const queryElements = this.query.filter(\n            (queryElem) => queryElem.searchItemId === searchItem.id\n        );\n        const isActive = Boolean(queryElements.length);\n        const enrichSearchItem = Object.assign({ isActive }, searchItem);\n        function _enrichOptions(options, selectedIds) {\n            return options.map((o) => {\n                const { description, id, groupNumber } = o;\n                const isActive = selectedIds.some((optionId) => optionId === id);\n                return { description, id, groupNumber, isActive };\n            });\n        }\n        switch (searchItem.type) {\n            case \"comparison\": {\n                const { dateFilterId } = searchItem;\n                const dateFilterIsActive = this.query.some(\n                    (queryElem) =>\n                        queryElem.searchItemId === dateFilterId &&\n                        !queryElem.generatorId.startsWith(\"custom\")\n                );\n                if (!dateFilterIsActive) {\n                    return null;\n                }\n                break;\n            }\n            case \"dateFilter\":\n                enrichSearchItem.options = _enrichOptions(\n                    getPeriodOptions(this.referenceMoment, searchItem.optionsParams),\n                    queryElements.map((queryElem) => queryElem.generatorId)\n                );\n                break;\n            case \"dateGroupBy\":\n                enrichSearchItem.options = _enrichOptions(\n                    this.intervalOptions,\n                    queryElements.map((queryElem) => queryElem.intervalId)\n                );\n                break;\n            case \"field\":\n            case \"field_property\":\n                enrichSearchItem.autocompleteValues = queryElements.map(\n                    (queryElem) => queryElem.autocompleteValue\n                );\n                break;\n        }\n        return enrichSearchItem;\n    }\n\n    /**\n     * Ensures that the active value of a category is one of its own\n     * existing values.\n     * @param {Category} category\n     * @param {number[]} valueIds\n     */\n    _ensureCategoryValue(category, valueIds) {\n        if (!valueIds.includes(category.activeValueId)) {\n            category.activeValueId = valueIds[0];\n        }\n    }\n\n    _extractSearchDefaultsFromGlobalContext() {\n        const searchDefaults = {};\n        const searchPanelDefaults = {};\n        for (const key in this.globalContext) {\n            const defaultValue = this.globalContext[key];\n            const searchDefaultMatch = /^search_default_(.*)$/.exec(key);\n            if (searchDefaultMatch) {\n                if (defaultValue) {\n                    searchDefaults[searchDefaultMatch[1]] = defaultValue;\n                }\n                delete this.globalContext[key];\n                continue;\n            }\n            const searchPanelDefaultMatch = /^searchpanel_default_(.*)$/.exec(key);\n            if (searchPanelDefaultMatch) {\n                searchPanelDefaults[searchPanelDefaultMatch[1]] = defaultValue;\n                delete this.globalContext[key];\n            }\n        }\n        return { searchDefaults, searchPanelDefaults };\n    }\n\n    /**\n     * Fetches values for each category at startup. At reload a category is\n     * only fetched if needed.\n     * @param {Category[]} categories\n     * @returns {Promise} resolved when all categories have been fetched\n     */\n    async _fetchCategories(categories) {\n        const filterDomain = this._getFilterDomain();\n        const searchDomain = this.searchDomain;\n        await Promise.all(\n            categories.map(async (category) => {\n                const result = await this.orm.call(\n                    this.resModel,\n                    \"search_panel_select_range\",\n                    [category.fieldName],\n                    {\n                        category_domain: this._getCategoryDomain(category.id),\n                        context: this.globalContext,\n                        enable_counters: category.enableCounters,\n                        expand: category.expand,\n                        filter_domain: filterDomain,\n                        hierarchize: category.hierarchize,\n                        limit: category.limit,\n                        search_domain: searchDomain,\n                    }\n                );\n                this._createCategoryTree(category.id, result);\n            })\n        );\n    }\n\n    /**\n     * Fetches values for each filter. This is done at startup and at each\n     * reload if needed.\n     * @param {Filter[]} filters\n     * @returns {Promise} resolved when all filters have been fetched\n     */\n    async _fetchFilters(filters) {\n        const evalContext = {};\n        for (const category of this.categories) {\n            evalContext[category.fieldName] = category.activeValueId;\n        }\n        const categoryDomain = this._getCategoryDomain();\n        const searchDomain = this.searchDomain;\n        await Promise.all(\n            filters.map(async (filter) => {\n                const result = await this.orm.call(\n                    this.resModel,\n                    \"search_panel_select_multi_range\",\n                    [filter.fieldName],\n                    {\n                        category_domain: categoryDomain,\n                        comodel_domain: new Domain(filter.domain).toList(evalContext),\n                        context: this.globalContext,\n                        enable_counters: filter.enableCounters,\n                        filter_domain: this._getFilterDomain(filter.id),\n                        expand: filter.expand,\n                        group_by: filter.groupBy || false,\n                        group_domain: this._getGroupDomain(filter),\n                        limit: filter.limit,\n                        search_domain: searchDomain,\n                    }\n                );\n                this._createFilterTree(filter.id, result);\n            })\n        );\n    }\n\n    /**\n     * Fetches values for the given categories and filters.\n     * @param {Category[]} categoriesToLoad\n     * @param {Filter[]} filtersToLoad\n     * @returns {Promise} resolved when all categories have been fetched\n     */\n    async _fetchSections(categoriesToLoad, filtersToLoad) {\n        await this._fetchCategories(categoriesToLoad);\n        await this._fetchFilters(filtersToLoad);\n        this.searchPanelInfo.loaded = true;\n    }\n\n    _getActiveComparison() {\n        for (const queryElem of this.query) {\n            const searchItem = this.searchItems[queryElem.searchItemId];\n            if (searchItem.type === \"comparison\") {\n                return searchItem;\n            }\n        }\n        return null;\n    }\n\n    /**\n     * Computes and returns the domain based on the current active\n     * categories. If \"excludedCategoryId\" is provided, the category with\n     * that id is not taken into account in the domain computation.\n     * @param {string} [excludedCategoryId]\n     * @returns {Array[]}\n     */\n    _getCategoryDomain(excludedCategoryId) {\n        const domain = [];\n        for (const category of this.categories) {\n            if (category.id === excludedCategoryId || !category.activeValueId) {\n                continue;\n            }\n            const field = this.searchViewFields[category.fieldName];\n            const operator = field.type === \"many2one\" && category.parentField ? \"child_of\" : \"=\";\n            domain.push([category.fieldName, operator, category.activeValueId]);\n        }\n        return domain;\n    }\n\n    /**\n     * Construct a single context from the contexts of\n     * filters of type 'filter', 'favorite', and 'field'.\n     * @returns {Object}\n     */\n    _getContext() {\n        const groups = this._getGroups();\n        const contexts = [user.context];\n        for (const group of groups) {\n            for (const activeItem of group.activeItems) {\n                const context = this._getSearchItemContext(activeItem);\n                if (context) {\n                    contexts.push(context);\n                }\n            }\n        }\n        let context;\n        try {\n            context = makeContext(contexts);\n            return context;\n        } catch (error) {\n            throw new Error(\n                _t(\"Failed to evaluate the context: %(context)s.\\n%(error)s\", {\n                    context,\n                    error: error.message,\n                })\n            );\n        }\n    }\n\n    /**\n     * Compute the string representation or the description of the current domain associated\n     * with a date filter starting from its corresponding query elements.\n     */\n    _getDateFilterDomain(dateFilter, generatorIds, key = \"domain\") {\n        const dateFilterRange = constructDateDomain(this.referenceMoment, dateFilter, generatorIds);\n        return dateFilterRange[key];\n    }\n\n    /**\n     * Returns which components are displayed in the current action. Components\n     * are opt-out, meaning that they will be displayed as long as a falsy\n     * value is not provided. With the search panel, the view type must also\n     * match the given (or default) search panel view types if the search model\n     * is instanciated in a view (this doesn't apply for any other action type).\n     * @private\n     * @param {Object} [display={}]\n     * @returns {{ controlPanel: Object | false, searchPanel: boolean, banner: boolean }}\n     */\n    _getDisplay(display = {}) {\n        const { viewTypes } = this.searchPanelInfo;\n        const { bannerRoute, viewType } = this.env.config;\n        return {\n            controlPanel: \"controlPanel\" in display ? display.controlPanel : {},\n            searchPanel:\n                this.sections.size &&\n                (!viewType || viewTypes.includes(viewType)) &&\n                (\"searchPanel\" in display ? display.searchPanel : true),\n            banner: Boolean(bannerRoute),\n        };\n    }\n\n    /**\n     * Return a domain created by combinining appropriately (with an 'AND') the domains\n     * coming from the active groups of type 'filter', 'dateFilter', 'favorite', and 'field'.\n     * @param {Object} [params]\n     * @param {boolean} [params.raw=false]\n     * @param {boolean} [params.withSearchPanel=true]\n     * @param {boolean} [params.withGlobal=true]\n     * @returns {DomainListRepr | Domain} Domain instance if 'raw', else the evaluated list domain\n     */\n    _getDomain(params = {}) {\n        const withSearchPanel = \"withSearchPanel\" in params ? params.withSearchPanel : true;\n        const withGlobal = \"withGlobal\" in params ? params.withGlobal : true;\n\n        const groups = this._getGroups();\n        const domains = [];\n        if (withGlobal) {\n            domains.push(this.globalDomain);\n        }\n        for (const group of groups) {\n            const groupActiveItemDomains = [];\n            for (const activeItem of group.activeItems) {\n                const domain = this._getSearchItemDomain(activeItem);\n                if (domain) {\n                    groupActiveItemDomains.push(domain);\n                }\n            }\n            const groupDomain = Domain.or(groupActiveItemDomains);\n            domains.push(groupDomain);\n        }\n\n        // we need to manage (optional) facets, deactivateGroup, clearQuery,...\n\n        if (this.display.searchPanel && withSearchPanel) {\n            domains.push(this._getSearchPanelDomain());\n        }\n\n        let domain;\n        try {\n            domain = Domain.and(domains);\n            return params.raw ? domain : domain.toList(this.domainEvalContext);\n        } catch (error) {\n            throw new Error(\n                _t(\"Failed to evaluate the domain: %(domain)s.\\n%(error)s\", {\n                    domain: domain.toString(),\n                    error: error.message,\n                })\n            );\n        }\n    }\n\n    _getFacets() {\n        const facets = [];\n        const groups = this._getGroups();\n        for (const group of groups) {\n            const groupActiveItemDomains = [];\n            const values = [];\n            let title;\n            let type;\n            for (const activeItem of group.activeItems) {\n                const domain = this._getSearchItemDomain(activeItem, {\n                    withDateFilterDomain: true,\n                });\n                if (domain) {\n                    groupActiveItemDomains.push(domain);\n                }\n                const searchItem = this.searchItems[activeItem.searchItemId];\n                switch (searchItem.type) {\n                    case \"field_property\":\n                    case \"field\": {\n                        type = \"field\";\n                        title = searchItem.description;\n                        for (const autocompleteValue of activeItem.autocompletValues) {\n                            values.push(autocompleteValue.label);\n                        }\n                        break;\n                    }\n                    case \"groupBy\": {\n                        type = \"groupBy\";\n                        values.push(searchItem.description);\n                        break;\n                    }\n                    case \"dateGroupBy\": {\n                        type = \"groupBy\";\n                        for (const intervalId of activeItem.intervalIds) {\n                            const option = this.intervalOptions.find((o) => o.id === intervalId);\n                            values.push(`${searchItem.description}: ${option.description}`);\n                        }\n                        break;\n                    }\n                    case \"dateFilter\": {\n                        type = \"filter\";\n                        const periodDescription = this._getDateFilterDomain(\n                            searchItem,\n                            activeItem.generatorIds,\n                            \"description\"\n                        );\n                        values.push(`${searchItem.description}: ${periodDescription}`);\n                        break;\n                    }\n                    default: {\n                        type = searchItem.type;\n                        values.push(searchItem.description);\n                    }\n                }\n            }\n            const facet = {\n                groupId: group.id,\n                type,\n                values,\n                separator: type === \"groupBy\" ? \">\" : _t(\"or\"),\n            };\n            if (type === \"field\") {\n                facet.title = title;\n            } else {\n                if (type === \"groupBy\" && this.orderByCount) {\n                    facet.icon =\n                        FACET_ICONS[this.orderByCount === \"Asc\" ? \"groupByAsc\" : \"groupByDesc\"];\n                } else {\n                    facet.icon = FACET_ICONS[type];\n                }\n                facet.color = FACET_COLORS[type];\n            }\n            if (groupActiveItemDomains.length) {\n                facet.domain = Domain.or(groupActiveItemDomains).toString();\n            }\n            facets.push(facet);\n        }\n\n        return facets;\n    }\n\n    /**\n     * Return the domain resulting from the combination of the autocomplete values\n     * of a search item of type 'field'.\n     */\n    _getFieldDomain(field, autocompleteValues) {\n        const domains = autocompleteValues.map(({ label, value, operator, enforceEqual }) => {\n            let domain;\n            if (field.filterDomain) {\n                let filterDomain = field.filterDomain;\n                if (enforceEqual) {\n                    filterDomain = field.filterDomain\n                        .replaceAll(\"'ilike'\", \"'='\")\n                        .replaceAll('\"ilike\"', '\"=\"');\n                }\n                domain = new Domain(filterDomain).toList({\n                    self: label.trim(),\n                    raw_value: value,\n                });\n            } else if (field.type === \"field\") {\n                domain = [[field.fieldName, operator, value]];\n            } else if (field.type === \"field_property\") {\n                domain = [\n                    field.propertyDomain,\n                    [`${field.fieldName}.${field.propertyFieldDefinition.name}`, operator, value],\n                ];\n            }\n            return new Domain(domain);\n        });\n        return Domain.or(domains);\n    }\n\n    /**\n     * Computes and returns the domain based on the current checked\n     * filters. The values of a single filter are combined using a simple\n     * rule: checked values within a same group are combined with an \"OR\"\n     * operator (this is expressed as single condition using a list) and\n     * groups are combined with an \"AND\" operator (expressed by\n     * concatenation of conditions).\n     * If a filter has no group, its checked values are implicitely\n     * considered as forming a group (and grouped using an \"OR\").\n     * If excludedFilterId is provided, the filter with that id is not\n     * taken into account in the domain computation.\n     * @param {string} [excludedFilterId]\n     * @returns {Array[]}\n     */\n    _getFilterDomain(excludedFilterId) {\n        const domain = [];\n\n        function addCondition(fieldName, valueMap) {\n            const ids = [];\n            for (const [valueId, value] of valueMap) {\n                if (value.checked) {\n                    ids.push(valueId);\n                }\n            }\n            if (ids.length) {\n                domain.push([fieldName, \"in\", ids]);\n            }\n        }\n\n        for (const filter of this.filters) {\n            if (filter.id === excludedFilterId) {\n                continue;\n            }\n            const { fieldName, groups, values } = filter;\n            if (groups) {\n                for (const group of groups.values()) {\n                    addCondition(fieldName, group.values);\n                }\n            } else {\n                addCondition(fieldName, values);\n            }\n        }\n        return domain;\n    }\n\n    /**\n     * Return the concatenation of groupBys comming from the active filters of\n     * type 'favorite' and 'groupBy'.\n     * The result respects the appropriate logic: the groupBys\n     * coming from an active favorite (if any) come first, then come the\n     * groupBys comming from the active filters of type 'groupBy' in the order\n     * defined in this.query. If no groupBys are found, one tries to\n     * find some grouBys in this.globalContext.\n     */\n    _getGroupBy() {\n        const groups = this._getGroups();\n        const groupBys = [];\n        for (const group of groups) {\n            for (const activeItem of group.activeItems) {\n                const activeItemGroupBys = this._getSearchItemGroupBys(activeItem);\n                if (activeItemGroupBys) {\n                    groupBys.push(...activeItemGroupBys);\n                }\n            }\n        }\n        const groupBy = groupBys.length ? groupBys : this.globalGroupBy.slice();\n        return typeof groupBy === \"string\" ? [groupBy] : groupBy;\n    }\n\n    /**\n     * Returns a domain or an object of domains used to complement\n     * the filter domains to accurately describe the constrains on\n     * records when computing record counts associated to the filter\n     * values (if a groupBy is provided). The idea is that the checked\n     * values within a group should not impact the counts for the other\n     * values in the same group.\n     * @param {Filter} filter\n     * @returns {Object<string, Array[]> | Array[] | null}\n     */\n    _getGroupDomain(filter) {\n        const { fieldName, groups, enableCounters } = filter;\n        const { type: fieldType } = this.searchViewFields[fieldName];\n\n        if (!enableCounters || !groups) {\n            return {\n                many2one: [],\n                many2many: {},\n            }[fieldType];\n        }\n        let groupDomain = null;\n        if (fieldType === \"many2one\") {\n            for (const group of groups.values()) {\n                const valueIds = [];\n                let active = false;\n                for (const [valueId, value] of group.values) {\n                    const { checked } = value;\n                    valueIds.push(valueId);\n                    if (checked) {\n                        active = true;\n                    }\n                }\n                if (active) {\n                    if (groupDomain) {\n                        groupDomain = [[0, \"=\", 1]];\n                        break;\n                    } else {\n                        groupDomain = [[fieldName, \"in\", valueIds]];\n                    }\n                }\n            }\n        } else if (fieldType === \"many2many\") {\n            const checkedValueIds = new Map();\n            groups.forEach(({ values }, groupId) => {\n                values.forEach(({ checked }, valueId) => {\n                    if (checked) {\n                        if (!checkedValueIds.has(groupId)) {\n                            checkedValueIds.set(groupId, []);\n                        }\n                        checkedValueIds.get(groupId).push(valueId);\n                    }\n                });\n            });\n            groupDomain = {};\n            for (const [gId, ids] of checkedValueIds.entries()) {\n                for (const groupId of groups.keys()) {\n                    if (gId !== groupId) {\n                        const key = JSON.stringify(groupId);\n                        if (!groupDomain[key]) {\n                            groupDomain[key] = [];\n                        }\n                        groupDomain[key].push([fieldName, \"in\", ids]);\n                    }\n                }\n            }\n        }\n        return groupDomain;\n    }\n\n    /**\n     * Reconstruct the (active) groups from the query elements.\n     * @returns {Object[]}\n     */\n    _getGroups() {\n        const preGroups = [];\n        for (const queryElem of this.query) {\n            const { searchItemId } = queryElem;\n            const { groupId } = this.searchItems[searchItemId];\n            let preGroup = preGroups.find((group) => group.id === groupId);\n            if (!preGroup) {\n                preGroup = { id: groupId, queryElements: [] };\n                preGroups.push(preGroup);\n            }\n            preGroup.queryElements.push(queryElem);\n        }\n        const groups = [];\n        for (const preGroup of preGroups) {\n            const { queryElements, id } = preGroup;\n            const activeItems = [];\n            for (const queryElem of queryElements) {\n                const { searchItemId } = queryElem;\n                let activeItem = activeItems.find(({ searchItemId: id }) => id === searchItemId);\n                if (\"generatorId\" in queryElem) {\n                    if (!activeItem) {\n                        activeItem = { searchItemId, generatorIds: [] };\n                        activeItems.push(activeItem);\n                    }\n                    activeItem.generatorIds.push(queryElem.generatorId);\n                } else if (\"intervalId\" in queryElem) {\n                    if (!activeItem) {\n                        activeItem = { searchItemId, intervalIds: [] };\n                        activeItems.push(activeItem);\n                    }\n                    activeItem.intervalIds.push(queryElem.intervalId);\n                } else if (\"autocompleteValue\" in queryElem) {\n                    if (!activeItem) {\n                        activeItem = { searchItemId, autocompletValues: [] };\n                        activeItems.push(activeItem);\n                    }\n                    activeItem.autocompletValues.push(queryElem.autocompleteValue);\n                } else {\n                    if (!activeItem) {\n                        activeItem = { searchItemId };\n                        activeItems.push(activeItem);\n                    }\n                }\n            }\n            for (const activeItem of activeItems) {\n                if (\"intervalIds\" in activeItem) {\n                    activeItem.intervalIds.sort((g1, g2) => rankInterval(g1) - rankInterval(g2));\n                }\n            }\n            groups.push({ id, activeItems });\n        }\n        return groups;\n    }\n\n    /**\n     *\n     * @private\n     * @param {Object} [params={}]\n     * @returns {{ preFavorite: Object, irFilter: Object }}\n     */\n    _getIrFilterDescription(params = {}) {\n        const { description, isDefault, isShared, embeddedActionId } = params;\n        const fns = this.env.__getContext__.callbacks;\n        const localContext = Object.assign({}, ...fns.map((fn) => fn()));\n        const gs = this.env.__getOrderBy__.callbacks;\n        let localOrderBy;\n        if (gs.length) {\n            localOrderBy = gs.flatMap((g) => g());\n        }\n        const context = makeContext([this._getContext(), localContext]);\n        const userContext = user.context;\n        for (const key in context) {\n            if (key in userContext || /^search(panel)?_default_/.test(key)) {\n                // clean search defaults and user context keys\n                delete context[key];\n            }\n        }\n        const domain = this._getDomain({ raw: true, withGlobal: false }).toString();\n        const groupBys = this._getGroupBy();\n        const comparison = this.getFullComparison();\n        const orderBy = localOrderBy || this._getOrderBy();\n        const userId = isShared ? false : user.userId;\n\n        const preFavorite = {\n            description,\n            isDefault,\n            domain,\n            context,\n            groupBys,\n            orderBy,\n            userId,\n        };\n        const irFilter = {\n            name: description,\n            action_id: this.env.config.actionId,\n            model_id: this.resModel,\n            domain,\n            embedded_action_id: embeddedActionId,\n            embedded_parent_res_id: this.globalContext.active_id || false,\n            is_default: isDefault,\n            sort: JSON.stringify(orderBy.map((o) => `${o.name}${o.asc === false ? \" desc\" : \"\"}`)),\n            user_id: userId,\n            context: { group_by: groupBys, ...context },\n        };\n\n        if (comparison) {\n            preFavorite.comparison = comparison;\n            irFilter.context.comparison = comparison;\n        }\n\n        return { preFavorite, irFilter };\n    }\n\n    /**\n     * @returns {OrderTerm[]}\n     */\n    _getOrderBy() {\n        const groups = this._getGroups();\n        const orderBy = [];\n        if (this.groupBy.length && this.orderByCount) {\n            orderBy.push({ name: \"__count\", asc: this.orderByCount === \"Asc\" });\n        }\n        for (const group of groups) {\n            for (const activeItem of group.activeItems) {\n                const { searchItemId } = activeItem;\n                const searchItem = this.searchItems[searchItemId];\n                if (searchItem.type === \"favorite\") {\n                    orderBy.push(...searchItem.orderBy);\n                }\n            }\n        }\n        return orderBy.length ? orderBy : this.globalOrderBy;\n    }\n\n    /**\n     * Return the context of the provided (active) filter.\n     */\n    _getSearchItemContext(activeItem) {\n        const { searchItemId } = activeItem;\n        const searchItem = this.searchItems[searchItemId];\n        switch (searchItem.type) {\n            case \"field\": {\n                // for <field> nodes, a dynamic context (like context=\"{'field1': self}\")\n                // should set {'field1': [value1, value2]} in the context\n                let context = {};\n                if (searchItem.context) {\n                    try {\n                        const self = activeItem.autocompletValues.map(\n                            (autocompleValue) => autocompleValue.value\n                        );\n                        context = evaluateExpr(searchItem.context, { self });\n                        if (typeof context !== \"object\") {\n                            throw Error();\n                        }\n                    } catch (error) {\n                        throw new Error(\n                            _t(\"Failed to evaluate the context: %(context)s.\\n%(error)s\", {\n                                context: searchItem.context,\n                                error: error.message,\n                            })\n                        );\n                    }\n                }\n                // the following code aims to remodel this:\n                // https://github.com/odoo/odoo/blob/12.0/addons/web/static/src/js/views/search/search_inputs.js#L498\n                // this is required for the helpdesk tour to pass\n                // this seems weird to only do that for m2o fields, but a test fails if\n                // we do it for other fields (my guess being that the test should simply\n                // be adapted)\n                if (searchItem.isDefault && searchItem.fieldType === \"many2one\") {\n                    context[`default_${searchItem.fieldName}`] =\n                        searchItem.defaultAutocompleteValue.value;\n                }\n                return context;\n            }\n            case \"favorite\":\n            case \"filter\": {\n                //Return a deep copy of the filter/favorite to avoid the view to modify the context\n                return makeContext([searchItem.context && deepCopy(searchItem.context)]);\n            }\n            default: {\n                return null;\n            }\n        }\n    }\n\n    /**\n     * Return the domain of the provided filter.\n     * @param {Object} [options={}]\n     * @param {boolean} [options.withDateFilterDomain]\n     */\n    _getSearchItemDomain(activeItem, options = {}) {\n        const { searchItemId } = activeItem;\n        const searchItem = this.searchItems[searchItemId];\n        switch (searchItem.type) {\n            case \"field_property\":\n            case \"field\": {\n                return this._getFieldDomain(searchItem, activeItem.autocompletValues);\n            }\n            case \"dateFilter\": {\n                const { dateFilterId } = this._getActiveComparison() || {};\n                if (\n                    options.withDateFilterDomain ||\n                    !(this.searchMenuTypes.has(\"comparison\") && dateFilterId === searchItemId)\n                ) {\n                    return this._getDateFilterDomain(searchItem, activeItem.generatorIds);\n                }\n                return new Domain([]);\n            }\n            case \"filter\":\n            case \"favorite\": {\n                return searchItem.domain;\n            }\n            default: {\n                return null;\n            }\n        }\n    }\n\n    _getSearchItemGroupBys(activeItem) {\n        const { searchItemId } = activeItem;\n        const searchItem = this.searchItems[searchItemId];\n        switch (searchItem.type) {\n            case \"dateGroupBy\": {\n                const { fieldName } = searchItem;\n                return activeItem.intervalIds.map((intervalId) => `${fieldName}:${intervalId}`);\n            }\n            case \"groupBy\": {\n                return [searchItem.fieldName];\n            }\n            case \"favorite\": {\n                return searchItem.groupBys;\n            }\n            default: {\n                return null;\n            }\n        }\n    }\n\n    /**\n     * Starting from a date filter id, returns the array of option ids currently selected\n     * for the corresponding date filter.\n     */\n    _getSelectedGeneratorIds(dateFilterId) {\n        const selectedOptionIds = [];\n        for (const queryElem of this.query) {\n            if (queryElem.searchItemId === dateFilterId && \"generatorId\" in queryElem) {\n                selectedOptionIds.push(queryElem.generatorId);\n            }\n        }\n        return selectedOptionIds;\n    }\n\n    /**\n     * @returns {Domain}\n     */\n    _getSearchPanelDomain() {\n        return Domain.and([this._getCategoryDomain(), this._getFilterDomain()]);\n    }\n\n    /**\n     * @param {Object} state\n     */\n    _importState(state) {\n        execute(arraytoMap, state, this);\n    }\n\n    /**\n     * @param {Object} irFilter\n     */\n    _irFilterToFavorite(irFilter) {\n        let userId = false;\n        if (Array.isArray(irFilter.user_id)) {\n            userId = irFilter.user_id[0];\n        }\n        const groupNumber = userId ? FAVORITE_PRIVATE_GROUP : FAVORITE_SHARED_GROUP;\n        const context = evaluateExpr(irFilter.context, user.context);\n        let groupBys = [];\n        if (context.group_by) {\n            groupBys = context.group_by;\n            delete context.group_by;\n        }\n        let comparison;\n        if (context.comparison) {\n            comparison = context.comparison;\n            if (typeof comparison.range === \"string\") {\n                // legacy case\n                comparison.range = new Domain(comparison.range).toList();\n            }\n            if (typeof comparison.comparisonRange === \"string\") {\n                // legacy case\n                comparison.comparisonRange = new Domain(comparison.comparisonRange).toList();\n            }\n            delete context.comparison;\n        }\n        let sort;\n        try {\n            sort = JSON.parse(irFilter.sort);\n        } catch (err) {\n            if (err instanceof SyntaxError) {\n                sort = [];\n            } else {\n                throw err;\n            }\n        }\n        const orderBy = sort.map((order) => {\n            let fieldName;\n            let asc;\n            const sqlNotation = order.split(\" \");\n            if (sqlNotation.length > 1) {\n                // regex: \\fieldName (asc|desc)?\\\n                fieldName = sqlNotation[0];\n                asc = sqlNotation[1] === \"asc\";\n            } else {\n                // legacy notation -- regex: \\-?fieldName\\\n                fieldName = order[0] === \"-\" ? order.slice(1) : order;\n                asc = order[0] === \"-\" ? false : true;\n            }\n            return {\n                asc: asc,\n                name: fieldName,\n            };\n        });\n        const favorite = {\n            context,\n            description: irFilter.name,\n            domain: irFilter.domain,\n            groupBys,\n            groupNumber,\n            orderBy,\n            removable: true,\n            serverSideId: irFilter.id,\n            type: \"favorite\",\n            userId,\n        };\n        if (irFilter.is_default) {\n            favorite.isDefault = irFilter.is_default;\n        }\n        if (comparison) {\n            favorite.comparison = comparison;\n        }\n        return favorite;\n    }\n\n    async _notify() {\n        if (this.blockNotification) {\n            return;\n        }\n\n        this._reset();\n\n        await this._reloadSections();\n\n        this.trigger(\"update\");\n    }\n\n    /**\n     * Updates the search domain and reloads sections if:\n     * - the current search domain is different from the previous, or...\n     * - a `shouldReload` flag has been set to true on the searchPanelInfo.\n     * The latter means that the search domain has been modified while the\n     * search panel was not displayed (and thus not reloaded) and the reload\n     * should occur as soon as the search panel is visible again.\n     * @private\n     * @returns {Promise<void>}\n     */\n    async _reloadSections() {\n        this.blockNotification = true;\n\n        // Check whether the search domain changed\n        const searchDomain = this._getDomain({ withSearchPanel: false });\n        const searchDomainChanged =\n            this.searchPanelInfo.shouldReload ||\n            JSON.stringify(this.searchDomain) !== JSON.stringify(searchDomain);\n        this.searchDomain = searchDomain;\n\n        // Check whether categories/filters will force a reload of the sections\n        const toFetch = (section) =>\n            section.enableCounters || (searchDomainChanged && !section.expand);\n        const categoriesToFetch = this.categories.filter(toFetch);\n        const filtersToFetch = this.filters.filter(toFetch);\n\n        if (searchDomainChanged || Boolean(categoriesToFetch.length + filtersToFetch.length)) {\n            if (this.display.searchPanel) {\n                this.sectionsPromise = this._fetchSections(categoriesToFetch, filtersToFetch);\n                if (this._shouldWaitForData(searchDomainChanged)) {\n                    await this.sectionsPromise;\n                }\n            }\n            // If no current search panel: will try to reload on next model update\n            this.searchPanelInfo.shouldReload = !this.display.searchPanel;\n        }\n\n        this.blockNotification = false;\n    }\n\n    _reset() {\n        delete this._comparison;\n        this._context = null;\n        this._domain = null;\n        this._groupBy = null;\n        this._orderBy = null;\n    }\n\n    /**\n     * Returns whether the query informations should be considered as ready\n     * before or after having (re-)fetched the sections data.\n     * @param {boolean} searchDomainChanged\n     * @returns {boolean}\n     */\n    _shouldWaitForData(searchDomainChanged) {\n        if (this.categories.length && this.filters.some((filter) => filter.domain !== \"[]\")) {\n            // Selected category value might affect the filter values\n            return true;\n        }\n        if (!this.searchDomain.length) {\n            // No search domain -> no need to check for expand\n            return false;\n        }\n        return [...this.sections.values()].some(\n            (section) => !section.expand && searchDomainChanged\n        );\n    }\n\n    /**\n     * Legacy compatibility: the imported state of a legacy search panel model\n     * extension doesn't include the arch information, i.e. the class name and\n     * view types. We have to extract those if they are not given.\n     * @param {Object} searchViewDescription\n     * @param {Object} searchViewFields\n     */\n    __legacyParseSearchPanelArchAnyway(searchViewDescription, searchViewFields) {\n        if (this.searchPanelInfo) {\n            return;\n        }\n\n        const parser = new SearchArchParser(searchViewDescription, searchViewFields);\n        const { searchPanelInfo } = parser.parse();\n\n        this.searchPanelInfo = { ...searchPanelInfo, loaded: false, shouldReload: false };\n    }\n}\n", "import { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useBus } from \"@web/core/utils/hooks\";\n\nimport {\n    Component,\n    onMounted,\n    onWillStart,\n    onWillUpdateProps,\n    reactive,\n    useEffect,\n    useRef,\n    useState,\n} from \"@odoo/owl\";\nimport { useSetupAction } from \"@web/search/action_hook\";\n\n//-------------------------------------------------------------------------\n// Helpers\n//-------------------------------------------------------------------------\n\nconst isFilter = (s) => s.type === \"filter\";\nconst isActiveCategory = (s) => s.type === \"category\" && s.activeValueId;\n\n/**\n * @param {Map<string | false, Object>} values\n * @returns {Object[]}\n */\nconst nameOfCheckedValues = (values) => {\n    const names = [];\n    for (const [, value] of values) {\n        if (value.checked) {\n            names.push(value.display_name);\n        }\n    }\n    return names;\n};\n\n/**\n * Search panel\n *\n * Represent an extension of the search interface located on the left side of\n * the view. It is divided in sections defined in a \"<searchpanel>\" node located\n * inside of a \"<search>\" arch. Each section is represented by a list of different\n * values (categories or ungrouped filters) or groups of values (grouped filters).\n * Its state is directly affected by its model (@see SearchModel).\n */\nexport class SearchPanel extends Component {\n    static template = \"web.SearchPanel\";\n    static props = {};\n    static components = {\n        Dropdown,\n    };\n    static subTemplates = {\n        section: \"web.SearchPanel.Section\",\n        category: \"web.SearchPanel.Category\",\n        filtersGroup: \"web.SearchPanel.FiltersGroup\",\n    };\n\n    setup() {\n        this.state = useState({\n            active: {},\n            expanded: {},\n            showMobileSearch: false,\n            sidebarExpanded: !this.env.searchModel.searchPanelInfo.fold,\n        });\n        this.hasImportedState = false;\n        this.root = useRef(\"root\");\n        this.scrollTop = 0;\n        this.dropdownStates = {};\n        this.width = \"10px\";\n\n        this.importState(this.env.searchPanelState);\n\n        useBus(this.env.searchModel, \"update\", async () => {\n            await this.env.searchModel.sectionsPromise;\n            this.updateActiveValues();\n            this.render();\n        });\n\n        useEffect(\n            (el) => {\n                if (el && this.hasImportedState) {\n                    el.style[\"min-width\"] = this.width;\n                    el.scroll({ top: this.scrollTop });\n                }\n            },\n            () => [this.root.el]\n        );\n\n        useSetupAction({\n            getGlobalState: () => {\n                return {\n                    searchPanel: this.exportState(),\n                };\n            },\n        });\n\n        onWillStart(async () => {\n            await this.env.searchModel.sectionsPromise;\n            this.expandDefaultValue();\n            this.updateActiveValues();\n        });\n\n        onWillUpdateProps(async () => {\n            await this.env.searchModel.sectionsPromise;\n            this.updateActiveValues();\n        });\n\n        onMounted(() => {\n            this.updateGroupHeadersChecked();\n        });\n    }\n\n    //---------------------------------------------------------------------\n    // Getters\n    //---------------------------------------------------------------------\n\n    get sections() {\n        return this.env.searchModel.getSections((s) => !s.empty);\n    }\n\n    //---------------------------------------------------------------------\n    // Public\n    //---------------------------------------------------------------------\n\n    exportState() {\n        const exported = {\n            expanded: this.state.expanded,\n            scrollTop: this.root.el?.scrollTop || 0,\n            sidebarExpanded: this.state.sidebarExpanded,\n            width: this.width,\n        };\n        return JSON.stringify(exported);\n    }\n\n    importState(state) {\n        this.hasImportedState = Boolean(state);\n        if (this.hasImportedState) {\n            this.state.expanded = state.expanded;\n            this.scrollTop = state.scrollTop;\n            this.state.sidebarExpanded = state.sidebarExpanded;\n            this.width = state.width;\n        }\n    }\n\n    //---------------------------------------------------------------------\n    // Protected\n    //---------------------------------------------------------------------\n\n    getDropdownState(sectionId) {\n        if (!this.dropdownStates[sectionId]) {\n            const state = reactive({\n                isOpen: false,\n                open: () => (state.isOpen = true),\n                close: () => (state.isOpen = false),\n            });\n            this.dropdownStates[sectionId] = reactive(state);\n        }\n        return this.dropdownStates[sectionId];\n    }\n\n    /**\n     * Expands category values holding the default value of a category.\n     */\n    expandDefaultValue() {\n        if (this.hasImportedState) {\n            return;\n        }\n        const categories = this.env.searchModel.getSections((s) => s.type === \"category\");\n        for (const category of categories) {\n            this.state.expanded[category.id] = {};\n            if (category.activeValueId) {\n                const ancestorIds = this.getAncestorValueIds(category, category.activeValueId);\n                for (const ancestorId of ancestorIds) {\n                    this.state.expanded[category.id][ancestorId] = true;\n                }\n            }\n        }\n    }\n\n    /**\n     * @param {Object} category\n     * @param {number} categoryValueId\n     * @returns {number[]} list of ids of the ancestors of the given value in\n     *   the given category.\n     */\n    getAncestorValueIds(category, categoryValueId) {\n        const { parentId } = category.values.get(categoryValueId);\n        return parentId ? [...this.getAncestorValueIds(category, parentId), parentId] : [];\n    }\n\n    /**\n     * Returns a formatted version of the active categories to populate\n     * the selection banner of the control panel summary.\n     * @returns {Object[]}\n     */\n    getCategorySelection() {\n        const activeCategories = this.env.searchModel.getSections(isActiveCategory);\n        const selection = [];\n        for (const category of activeCategories) {\n            const parentIds = this.getAncestorValueIds(category, category.activeValueId);\n            const orderedCategoryNames = [...parentIds, category.activeValueId].map(\n                (valueId) => category.values.get(valueId).display_name\n            );\n            selection.push({\n                values: orderedCategoryNames,\n                icon: category.icon,\n                color: category.color,\n            });\n        }\n        return selection;\n    }\n\n    /**\n     * Returns a formatted version of the active filters to populate\n     * the selection banner of the control panel summary.\n     * @returns {Object[]}\n     */\n    getFilterSelection() {\n        const filters = this.env.searchModel.getSections(isFilter);\n        const selection = [];\n        for (const { groups, values, icon, color } of filters) {\n            let filterValues;\n            if (groups) {\n                filterValues = Object.keys(groups)\n                    .map((groupId) => nameOfCheckedValues(groups[groupId].values))\n                    .flat();\n            } else if (values) {\n                filterValues = nameOfCheckedValues(values);\n            }\n            if (filterValues.length) {\n                selection.push({ values: filterValues, icon, color });\n            }\n        }\n        return selection;\n    }\n\n    /**\n     * Checks if the section matching the provided id has at least one active selection.\n     * If no id is provided, checks if at least one section has an active selection.\n     * @param {Number} sectionId\n     */\n    hasSelection(sectionId = 0) {\n        if (sectionId) {\n            const sectionState = this.state.active[sectionId];\n            if (sectionState instanceof Object) {\n                return Object.values(sectionState).some((val) => val);\n            }\n            return Boolean(sectionState);\n        }\n        return Object.keys(this.state.active).some((key) => this.hasSelection(key));\n    }\n\n    /**\n     * Clears all active selection in the section which id was provided.\n     * If no id is provided, clears the selection of all sections.\n     * @param {Number} sectionId\n     */\n    clearSelection(sectionId = 0) {\n        const sectionIds = sectionId ? [sectionId] : Object.keys(this.state.active).map(Number);\n        this.env.searchModel.clearSections(sectionIds);\n    }\n\n    /**\n     * Prevent unnecessary calls to the model by ensuring a different category\n     * is clicked.\n     * @param {Object} category\n     * @param {Object} value\n     */\n    async toggleCategory(category, value) {\n        if (value.childrenIds.length) {\n            const categoryState = this.state.expanded[category.id];\n            if (categoryState[value.id] && category.activeValueId === value.id) {\n                delete categoryState[value.id];\n            } else {\n                categoryState[value.id] = true;\n            }\n        } else {\n            this.getDropdownState(category.id).close();\n        }\n        if (category.activeValueId !== value.id) {\n            this.env.searchModel.toggleCategoryValue(category.id, value.id);\n        }\n    }\n\n    toggleSidebar() {\n        this.state.sidebarExpanded = !this.state.sidebarExpanded;\n    }\n\n    /**\n     * @param {number} filterId\n     * @param {{ values: Map<Object> }} group\n     */\n    toggleFilterGroup(filterId, { values }) {\n        const valueIds = [];\n        const checked = [...values.values()].every(\n            (value) => this.state.active[filterId][value.id]\n        );\n        values.forEach(({ id }) => {\n            valueIds.push(id);\n            this.state.active[filterId][id] = !checked;\n        });\n        this.env.searchModel.toggleFilterValues(filterId, valueIds, !checked);\n    }\n\n    /**\n     * @param {number} filterId\n     * @param {Object} [group]\n     * @param {number} valueId\n     * @param {MouseEvent} ev\n     */\n    toggleFilterValue(filterId, valueId, { currentTarget }) {\n        this.state.active[filterId][valueId] = currentTarget.checked;\n        this.updateGroupHeadersChecked();\n        this.env.searchModel.toggleFilterValues(filterId, [valueId]);\n    }\n\n    updateActiveValues() {\n        for (const section of this.sections) {\n            if (section.type === \"category\") {\n                this.state.active[section.id] = section.activeValueId;\n            } else {\n                this.state.active[section.id] = {};\n                if (section.groups) {\n                    for (const group of section.groups.values()) {\n                        for (const value of group.values.values()) {\n                            this.state.active[section.id][value.id] = value.checked;\n                        }\n                    }\n                }\n                if (section && section.values) {\n                    for (const value of section.values.values()) {\n                        this.state.active[section.id][value.id] = value.checked;\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Updates the \"checked\" or \"indeterminate\" state of each of the group\n     * headers according to the state of their values.\n     */\n    updateGroupHeadersChecked() {\n        const groups = document.querySelectorAll(\".o_search_panel_filter_group\");\n        for (const group of groups) {\n            const header = group.querySelector(\":scope .o_search_panel_group_header input\");\n            const vals = [...group.querySelectorAll(\":scope .o_search_panel_filter_value input\")];\n            header.checked = false;\n            header.indeterminate = false;\n            if (vals.every((v) => v.checked)) {\n                header.checked = true;\n            } else if (vals.some((v) => v.checked)) {\n                header.indeterminate = true;\n            }\n        }\n    }\n\n    /**\n     * Handles the resize feature on the sidebar\n     *\n     * @private\n     * @param {PointerEvent} ev\n     */\n    _onStartResize(ev) {\n        // Only triggered by left mouse button\n        if (ev.button !== 0) {\n            return;\n        }\n\n        const initialX = ev.pageX;\n        const initialWidth = this.root.el.offsetWidth;\n        const resizeStoppingEvents = [\"keydown\", \"pointerdown\", \"pointerup\"];\n\n        // Pointermove event : resize header\n        const resizePanel = (ev) => {\n            ev.preventDefault();\n            ev.stopPropagation();\n            const maxWidth = Math.max(0.5 * window.innerWidth, initialWidth);\n            const delta = ev.pageX - initialX;\n            const newWidth = Math.min(maxWidth, Math.max(10, initialWidth + delta));\n            this.width = `${newWidth}px`;\n            this.root.el.style[\"min-width\"] = this.width;\n        };\n        document.addEventListener(\"pointermove\", resizePanel, true);\n\n        // Pointer or keyboard events : stop resize\n        const stopResize = (ev) => {\n            // Ignores the initial 'left mouse button down' event in order\n            // to not instantly remove the listener\n            if (ev.type === \"pointerdown\" && ev.button === 0) {\n                return;\n            }\n            ev.preventDefault();\n            ev.stopPropagation();\n\n            document.removeEventListener(\"pointermove\", resizePanel, true);\n            resizeStoppingEvents.forEach((stoppingEvent) => {\n                document.removeEventListener(stoppingEvent, stopResize, true);\n            });\n            // we remove the focus to make sure that the there is no focus inside\n            // the panel. If that is the case, there is some css to darken the whole\n            // thead, and it looks quite weird with the small css hover effect.\n            document.activeElement.blur();\n        };\n        // We have to listen to several events to properly stop the resizing function. Those are:\n        // - pointerdown (e.g. pressing right click)\n        // - pointerup : logical flow of the resizing feature (drag & drop)\n        // - keydown : (e.g. pressing 'Alt' + 'Tab' or 'Windows' key)\n        resizeStoppingEvents.forEach((stoppingEvent) => {\n            document.addEventListener(stoppingEvent, stopResize, true);\n        });\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Domain } from \"@web/core/domain\";\nimport { serializeDate, serializeDateTime } from \"@web/core/l10n/dates\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { clamp } from \"@web/core/utils/numbers\";\nimport { pick } from \"@web/core/utils/objects\";\n\nexport const QUARTERS = {\n    1: { description: _t(\"Q1\"), coveredMonths: [1, 2, 3] },\n    2: { description: _t(\"Q2\"), coveredMonths: [4, 5, 6] },\n    3: { description: _t(\"Q3\"), coveredMonths: [7, 8, 9] },\n    4: { description: _t(\"Q4\"), coveredMonths: [10, 11, 12] },\n};\n\nexport const QUARTER_OPTIONS = {\n    fourth_quarter: {\n        id: \"fourth_quarter\",\n        groupNumber: 1,\n        description: QUARTERS[4].description,\n        setParam: { quarter: 4 },\n        granularity: \"quarter\",\n    },\n    third_quarter: {\n        id: \"third_quarter\",\n        groupNumber: 1,\n        description: QUARTERS[3].description,\n        setParam: { quarter: 3 },\n        granularity: \"quarter\",\n    },\n    second_quarter: {\n        id: \"second_quarter\",\n        groupNumber: 1,\n        description: QUARTERS[2].description,\n        setParam: { quarter: 2 },\n        granularity: \"quarter\",\n    },\n    first_quarter: {\n        id: \"first_quarter\",\n        groupNumber: 1,\n        description: QUARTERS[1].description,\n        setParam: { quarter: 1 },\n        granularity: \"quarter\",\n    },\n};\n\nexport const DEFAULT_INTERVAL = \"month\";\n\nexport const INTERVAL_OPTIONS = {\n    year: { description: _t(\"Year\"), id: \"year\", groupNumber: 1 },\n    quarter: { description: _t(\"Quarter\"), id: \"quarter\", groupNumber: 1 },\n    month: { description: _t(\"Month\"), id: \"month\", groupNumber: 1 },\n    week: { description: _t(\"Week\"), id: \"week\", groupNumber: 1 },\n    day: { description: _t(\"Day\"), id: \"day\", groupNumber: 1 },\n};\n\n// ComparisonMenu parameters\nexport const COMPARISON_OPTIONS = {\n    previous_period: {\n        description: _t(\"Previous Period\"),\n        id: \"previous_period\",\n    },\n    previous_year: {\n        description: _t(\"Previous Year\"),\n        id: \"previous_year\",\n        plusParam: { years: -1 },\n    },\n};\n\nexport const PER_YEAR = {\n    year: 1,\n    quarter: 4,\n    month: 12,\n};\n\n//-------------------------------------------------------------------------\n// Functions\n//-------------------------------------------------------------------------\n\n/**\n * Constructs the string representation of a domain and its description. The\n * domain is of the form:\n *      ['|', d_1 ,..., '|', d_n]\n * where d_i is a time range of the form\n *      ['&', [fieldName, >=, leftBound_i], [fieldName, <=, rightBound_i]]\n * where leftBound_i and rightBound_i are date or datetime computed accordingly\n * to the given options and reference moment.\n */\nexport function constructDateDomain(\n    referenceMoment,\n    searchItem,\n    selectedOptionIds,\n    comparisonOptionId\n) {\n    let plusParam;\n    let selectedOptions;\n    if (comparisonOptionId) {\n        [plusParam, selectedOptions] = getComparisonParams(\n            referenceMoment,\n            searchItem,\n            selectedOptionIds,\n            comparisonOptionId\n        );\n    } else {\n        selectedOptions = getSelectedOptions(referenceMoment, searchItem, selectedOptionIds);\n    }\n    if (\"withDomain\" in selectedOptions) {\n        return {\n            description: selectedOptions.withDomain[0].description,\n            domain: Domain.and([selectedOptions.withDomain[0].domain, searchItem.domain]),\n        };\n    }\n    const yearOptions = selectedOptions.year;\n    const otherOptions = [...(selectedOptions.quarter || []), ...(selectedOptions.month || [])];\n    sortPeriodOptions(yearOptions);\n    sortPeriodOptions(otherOptions);\n    const ranges = [];\n    const { fieldName, fieldType } = searchItem;\n    for (const yearOption of yearOptions) {\n        const constructRangeParams = {\n            referenceMoment,\n            fieldName,\n            fieldType,\n            plusParam,\n        };\n        if (otherOptions.length) {\n            for (const option of otherOptions) {\n                const setParam = Object.assign(\n                    {},\n                    yearOption.setParam,\n                    option ? option.setParam : {}\n                );\n                const { granularity } = option;\n                const range = constructDateRange(\n                    Object.assign({ granularity, setParam }, constructRangeParams)\n                );\n                ranges.push(range);\n            }\n        } else {\n            const { granularity, setParam } = yearOption;\n            const range = constructDateRange(\n                Object.assign({ granularity, setParam }, constructRangeParams)\n            );\n            ranges.push(range);\n        }\n    }\n    let domain = Domain.combine(\n        ranges.map((range) => range.domain),\n        \"OR\"\n    );\n    domain = Domain.and([domain, searchItem.domain]);\n    const description = ranges.map((range) => range.description).join(\"/\");\n    return { domain, description };\n}\n\n/**\n * Constructs the string representation of a domain and its description. The\n * domain is a time range of the form:\n *      ['&', [fieldName, >=, leftBound],[fieldName, <=, rightBound]]\n * where leftBound and rightBound are some date or datetime determined by setParam,\n * plusParam, granularity and the reference moment.\n */\nexport function constructDateRange(params) {\n    const { referenceMoment, fieldName, fieldType, granularity, setParam, plusParam } = params;\n    if (\"quarter\" in setParam) {\n        // Luxon does not consider quarter key in setParam (like moment did)\n        setParam.month = QUARTERS[setParam.quarter].coveredMonths[0];\n        delete setParam.quarter;\n    }\n    const date = referenceMoment.set(setParam).plus(plusParam || {});\n    // compute domain\n    const leftDate = date.startOf(granularity);\n    const rightDate = date.endOf(granularity);\n    let leftBound;\n    let rightBound;\n    if (fieldType === \"date\") {\n        leftBound = serializeDate(leftDate);\n        rightBound = serializeDate(rightDate);\n    } else {\n        leftBound = serializeDateTime(leftDate);\n        rightBound = serializeDateTime(rightDate);\n    }\n    const domain = new Domain([\"&\", [fieldName, \">=\", leftBound], [fieldName, \"<=\", rightBound]]);\n    // compute description\n    const descriptions = [date.toFormat(\"yyyy\")];\n    const method = localization.direction === \"rtl\" ? \"push\" : \"unshift\";\n    if (granularity === \"month\") {\n        descriptions[method](date.toFormat(\"MMMM\"));\n    } else if (granularity === \"quarter\") {\n        const quarter = date.quarter;\n        descriptions[method](QUARTERS[quarter].description.toString());\n    }\n    const description = descriptions.join(\" \");\n    return { domain, description };\n}\n\n/**\n * Returns a version of the options in COMPARISON_OPTIONS with translated descriptions.\n * @see getOptionsWithDescriptions\n */\nexport function getComparisonOptions() {\n    return getOptionsWithDescriptions(COMPARISON_OPTIONS);\n}\n\n/**\n * Returns the params plusParam and selectedOptions necessary for the computation\n * of a comparison domain.\n */\nexport function getComparisonParams(\n    referenceMoment,\n    searchItem,\n    selectedOptionIds,\n    comparisonOptionId\n) {\n    const comparisonOption = COMPARISON_OPTIONS[comparisonOptionId];\n    const selectedOptions = getSelectedOptions(referenceMoment, searchItem, selectedOptionIds);\n    if (comparisonOption.plusParam) {\n        return [comparisonOption.plusParam, selectedOptions];\n    }\n    const plusParam = {};\n    let globalGranularity = \"year\";\n    if (selectedOptions.month) {\n        globalGranularity = \"month\";\n    } else if (selectedOptions.quarter) {\n        globalGranularity = \"quarter\";\n    }\n    const granularityFactor = PER_YEAR[globalGranularity];\n    const years = selectedOptions.year.map((o) => o.setParam.year);\n    const yearMin = Math.min(...years);\n    const yearMax = Math.max(...years);\n    let optionMin = 0;\n    let optionMax = 0;\n    if (selectedOptions.quarter) {\n        const quarters = selectedOptions.quarter.map((o) => o.setParam.quarter);\n        if (globalGranularity === \"month\") {\n            delete selectedOptions.quarter;\n            for (const quarter of quarters) {\n                for (const month of QUARTERS[quarter].coveredMonths) {\n                    const monthOption = selectedOptions.month.find(\n                        (o) => o.setParam.month === month\n                    );\n                    if (!monthOption) {\n                        selectedOptions.month.push({\n                            setParam: { month },\n                            granularity: \"month\",\n                        });\n                    }\n                }\n            }\n        } else {\n            optionMin = Math.min(...quarters);\n            optionMax = Math.max(...quarters);\n        }\n    }\n    if (selectedOptions.month) {\n        const months = selectedOptions.month.map((o) => o.setParam.month);\n        optionMin = Math.min(...months);\n        optionMax = Math.max(...months);\n    }\n    const num = -1 + granularityFactor * (yearMin - yearMax) + optionMin - optionMax;\n    const key =\n        globalGranularity === \"year\"\n            ? \"years\"\n            : globalGranularity === \"month\"\n            ? \"months\"\n            : \"quarters\";\n    plusParam[key] = num;\n    return [plusParam, selectedOptions];\n}\n\n/**\n * Returns a version of the options in INTERVAL_OPTIONS with translated descriptions.\n * @see getOptionsWithDescriptions\n */\nexport function getIntervalOptions() {\n    return getOptionsWithDescriptions(INTERVAL_OPTIONS);\n}\n\n/**\n * Returns a version of the options in OPTIONS with translated descriptions (if any).\n * @param {Object{}} OPTIONS\n * @returns {Object[]}\n */\nexport function getOptionsWithDescriptions(OPTIONS) {\n    const options = [];\n    for (const option of Object.values(OPTIONS)) {\n        options.push(Object.assign({}, option, { description: option.description.toString() }));\n    }\n    return options;\n}\n\n/**\n * Returns the period options relative to the referenceMoment for a date filter, with translated\n * descriptions and a key defautlYearId used in the control panel model when toggling a period option.\n */\nexport function getPeriodOptions(referenceMoment, optionsParams) {\n    return [\n        ...getMonthPeriodOptions(referenceMoment, optionsParams),\n        ...getQuarterPeriodOptions(optionsParams),\n        ...getYearPeriodOptions(referenceMoment, optionsParams),\n        ...getCustomPeriodOptions(optionsParams),\n    ];\n}\n\nexport function toGeneratorId(unit, offset) {\n    if (!offset) {\n        return unit;\n    }\n    const sep = offset > 0 ? \"+\" : \"-\";\n    const val = Math.abs(offset);\n    return `${unit}${sep}${val}`;\n}\n\nfunction getMonthPeriodOptions(referenceMoment, optionsParams) {\n    const { startYear, endYear, startMonth, endMonth } = optionsParams;\n    return [...Array(endMonth - startMonth + 1).keys()]\n        .map((i) => {\n            const monthOffset = startMonth + i;\n            const date = referenceMoment.plus({\n                months: monthOffset,\n                years: clamp(0, startYear, endYear),\n            });\n            const yearOffset = date.year - referenceMoment.year;\n            return {\n                id: toGeneratorId(\"month\", monthOffset),\n                defaultYearId: toGeneratorId(\"year\", clamp(yearOffset, startYear, endYear)),\n                description: date.toFormat(\"MMMM\"),\n                granularity: \"month\",\n                groupNumber: 1,\n                plusParam: { months: monthOffset },\n            };\n        })\n        .reverse();\n}\n\nfunction getQuarterPeriodOptions(optionsParams) {\n    const { startYear, endYear } = optionsParams;\n    const defaultYearId = toGeneratorId(\"year\", clamp(0, startYear, endYear));\n    return Object.values(QUARTER_OPTIONS).map((quarter) => ({\n        ...quarter,\n        defaultYearId,\n    }));\n}\n\nfunction getYearPeriodOptions(referenceMoment, optionsParams) {\n    const { startYear, endYear } = optionsParams;\n    return [...Array(endYear - startYear + 1).keys()]\n        .map((i) => {\n            const offset = startYear + i;\n            const date = referenceMoment.plus({ years: offset });\n            return {\n                id: toGeneratorId(\"year\", offset),\n                description: date.toFormat(\"yyyy\"),\n                granularity: \"year\",\n                groupNumber: 2,\n                plusParam: { years: offset },\n            };\n        })\n        .reverse();\n}\n\nfunction getCustomPeriodOptions(optionsParams) {\n    const { customOptions } = optionsParams;\n    return customOptions.map((option) => ({\n        id: option.id,\n        description: option.description,\n        granularity: \"withDomain\",\n        groupNumber: 3,\n        domain: option.domain,\n    }));\n}\n\n/**\n * Returns a partial version of the period options whose ids are in selectedOptionIds\n * partitioned by granularity.\n */\nexport function getSelectedOptions(referenceMoment, searchItem, selectedOptionIds) {\n    const selectedOptions = { year: [] };\n    const periodOptions = getPeriodOptions(referenceMoment, searchItem.optionsParams);\n    for (const optionId of selectedOptionIds) {\n        const option = periodOptions.find((option) => option.id === optionId);\n        const granularity = option.granularity;\n        if (!selectedOptions[granularity]) {\n            selectedOptions[granularity] = [];\n        }\n        if (option.domain) {\n            selectedOptions[granularity].push(pick(option, \"domain\", \"description\"));\n        } else {\n            const setParam = getSetParam(option, referenceMoment);\n            selectedOptions[granularity].push({ granularity, setParam });\n        }\n    }\n    return selectedOptions;\n}\n\n/**\n * Returns the setParam object associated with the given periodOption and\n * referenceMoment.\n */\nexport function getSetParam(periodOption, referenceMoment) {\n    if (periodOption.granularity === \"quarter\") {\n        return periodOption.setParam;\n    }\n    const date = referenceMoment.plus(periodOption.plusParam);\n    const granularity = periodOption.granularity;\n    const setParam = { [granularity]: date[granularity] };\n    return setParam;\n}\n\nexport function rankInterval(intervalOptionId) {\n    return Object.keys(INTERVAL_OPTIONS).indexOf(intervalOptionId);\n}\n\n/**\n * Sorts in place an array of 'period' options.\n */\nexport function sortPeriodOptions(options) {\n    options.sort((o1, o2) => {\n        var _a, _b;\n        const granularity1 = o1.granularity;\n        const granularity2 = o2.granularity;\n        if (granularity1 === granularity2) {\n            return (\n                ((_a = o1.setParam[granularity1]) !== null && _a !== void 0 ? _a : 0) -\n                ((_b = o2.setParam[granularity1]) !== null && _b !== void 0 ? _b : 0)\n            );\n        }\n        return granularity1 < granularity2 ? -1 : 1;\n    });\n}\n\n/**\n * Checks if a year id is among the given array of period option ids.\n */\nexport function yearSelected(selectedOptionIds) {\n    return selectedOptionIds.some((optionId) => optionId.startsWith(\"year\"));\n}\n", "import { DEFAULT_INTERVAL, INTERVAL_OPTIONS } from \"./dates\";\n\n/**\n * @param {string} descr\n */\nfunction errorMsg(descr) {\n    return `Invalid groupBy description: ${descr}`;\n}\n\n/**\n * @param {string} descr\n * @param {Object} fields\n * @returns {Object}\n */\nexport function getGroupBy(descr, fields) {\n    let fieldName;\n    let interval;\n    let spec;\n    [fieldName, interval] = descr.split(\":\");\n    if (!fieldName) {\n        throw Error();\n    }\n    if (fields) {\n        if (!fields[fieldName] && !fieldName.includes(\".\")) {\n            throw Error(errorMsg(descr));\n        }\n        const fieldType = fields[fieldName]?.type;\n        if ([\"date\", \"datetime\"].includes(fieldType)) {\n            if (!interval) {\n                interval = DEFAULT_INTERVAL;\n            } else if (!Object.keys(INTERVAL_OPTIONS).includes(interval)) {\n                throw Error(errorMsg(descr));\n            }\n            spec = `${fieldName}:${interval}`;\n        } else if (interval) {\n            throw Error(errorMsg(descr));\n        } else {\n            spec = fieldName;\n            interval = null;\n        }\n    } else {\n        if (interval) {\n            if (!Object.keys(INTERVAL_OPTIONS).includes(interval)) {\n                throw Error(errorMsg(descr));\n            }\n            spec = `${fieldName}:${interval}`;\n        } else {\n            spec = fieldName;\n            interval = null;\n        }\n    }\n    return {\n        fieldName,\n        interval,\n        spec,\n        toJSON() {\n            return spec;\n        },\n    };\n}\n", "export const FACET_ICONS = {\n    filter: \"fa fa-filter\",\n    groupBy: \"oi oi-group\",\n    groupByAsc: \"fa fa-sort-numeric-asc\",\n    groupByDesc: \"fa fa-sort-numeric-desc\",\n    favorite: \"fa fa-star\",\n    comparison: \"fa fa-adjust\",\n};\n\nexport const FACET_COLORS = {\n    filter: \"primary\",\n    groupBy: \"action\",\n    favorite: \"warning\",\n    comparison: \"danger\",\n};\n\nexport const GROUPABLE_TYPES = [\n    \"boolean\",\n    \"char\",\n    \"date\",\n    \"datetime\",\n    \"integer\",\n    \"many2one\",\n    \"many2many\",\n    \"selection\",\n    \"tags\",\n];\n", "/**\n * @typedef {Object} OrderTerm\n * @property {string} name\n * @property {boolean} asc\n */\n\n/**\n * @param {OrderTerm[]} orderBy\n * @returns {string}\n */\nexport function orderByToString(orderBy) {\n    return orderBy.map((o) => `${o.name} ${o.asc ? \"ASC\" : \"DESC\"}`).join(\", \");\n}\n\n/**\n * @param {any} string\n * @return {OrderTerm[]}\n */\nexport function stringToOrderBy(string) {\n    if (!string) {\n        return [];\n    }\n    return string.split(\",\").map((order) => {\n        const splitOrder = order.trim().split(\" \");\n        if (splitOrder.length === 2) {\n            return {\n                name: splitOrder[0],\n                asc: splitOrder[1].toLowerCase() === \"asc\",\n            };\n        } else {\n            return {\n                name: splitOrder[0],\n                asc: true,\n            };\n        }\n    });\n}\n", "import { Component, onWillStart, onWillUpdateProps, toRaw, useSubEnv } from \"@odoo/owl\";\nimport { CallbackRecorder, useSetupAction } from \"@web/search/action_hook\";\nimport { SearchModel } from \"@web/search/search_model\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\n\nexport const SEARCH_KEYS = [\"comparison\", \"context\", \"domain\", \"groupBy\", \"orderBy\"];\n\nexport class WithSearch extends Component {\n    static template = \"web.WithSearch\";\n    static props = {\n        slots: Object,\n        SearchModel: { type: Function, optional: true },\n\n        resModel: String,\n\n        globalState: { type: Object, optional: true },\n        searchModelArgs: { type: Object, optional: true },\n\n        display: { type: Object, optional: true },\n\n        // search query elements\n        comparison: { type: [Object, { value: null }], optional: true },\n        context: { type: Object, optional: true },\n        domain: { type: Array, element: [String, Array], optional: true },\n        groupBy: { type: Array, element: String, optional: true },\n        orderBy: { type: Array, element: Object, optional: true },\n\n        // search view description\n        searchViewArch: { type: String, optional: true },\n        searchViewFields: { type: Object, optional: true },\n        searchViewId: { type: [Number, Boolean], optional: true },\n\n        irFilters: { type: Array, element: Object, optional: true },\n        loadIrFilters: { type: Boolean, optional: true },\n\n        // extra options\n        activateFavorite: { type: Boolean, optional: true },\n        dynamicFilters: { type: Array, element: Object, optional: true },\n        hideCustomGroupBy: { type: Boolean, optional: true },\n        searchMenuTypes: { type: Array, element: String, optional: true },\n        canOrderByCount: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        if (!this.env.__getContext__) {\n            useSubEnv({ __getContext__: new CallbackRecorder() });\n        }\n        if (!this.env.__getOrderBy__) {\n            useSubEnv({ __getOrderBy__: new CallbackRecorder() });\n        }\n\n        const SearchModelClass = this.props.SearchModel || SearchModel;\n        this.searchModel = new SearchModelClass(\n            this.env,\n            {\n                orm: useService(\"orm\"),\n                view: useService(\"view\"),\n                field: useService(\"field\"),\n                name: useService(\"name\"),\n                dialog: useService(\"dialog\"),\n            },\n            this.props.searchModelArgs\n        );\n\n        const searchPanelState = this.props.globalState?.searchPanel\n            ? JSON.parse(this.props.globalState?.searchPanel)\n            : null;\n        useSubEnv({ searchModel: this.searchModel, searchPanelState });\n\n        useBus(this.searchModel, \"update\", this.render);\n        useSetupAction({\n            getGlobalState: () => {\n                return {\n                    searchModel: JSON.stringify(this.searchModel.exportState()),\n                };\n            },\n        });\n\n        onWillStart(async () => {\n            const config = { ...toRaw(this.props) };\n            if (config.globalState && config.globalState.searchModel) {\n                config.state = JSON.parse(config.globalState.searchModel);\n                delete config.globalState;\n            }\n            await this.searchModel.load(config);\n        });\n\n        onWillUpdateProps(async (nextProps) => {\n            const config = {};\n            for (const key of SEARCH_KEYS) {\n                if (nextProps[key] !== undefined) {\n                    config[key] = nextProps[key];\n                }\n            }\n            await this.searchModel.reload(config);\n        });\n    }\n}\n", "import { browser } from \"@web/core/browser/browser\";\nimport { evaluateExpr } from \"@web/core/py_js/py\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { visitXML } from \"@web/core/utils/xml\";\nimport { Field } from \"@web/views/fields/field\";\n\nconst FIELD_ATTRIBUTE_NAMES = [\n    \"date_start\",\n    \"date_delay\",\n    \"date_stop\",\n    \"all_day\",\n    \"recurrence_update\",\n    \"create_name_field\",\n    \"color\",\n];\nconst SCALES = [\"day\", \"week\", \"month\", \"year\"];\n\nexport class CalendarParseArchError extends Error {}\n\nexport class CalendarArchParser {\n    parse(arch, models, modelName) {\n        const fields = models[modelName].fields;\n        const fieldNames = new Set(fields.display_name ? [\"display_name\"] : []);\n        const fieldMapping = { date_start: \"date_start\" };\n        let jsClass = null;\n        let eventLimit = 5;\n        let scales = [...SCALES];\n        const sessionScale = browser.sessionStorage.getItem(\"calendar-scale\");\n        let scale = sessionScale || \"week\";\n        let canCreate = true;\n        let canDelete = true;\n        let canEdit = true;\n        let quickCreate = true;\n        let quickCreateViewId = null;\n        let hasEditDialog = false;\n        let showUnusualDays = false;\n        let isDateHidden = false;\n        let isTimeHidden = false;\n        let formViewId = false;\n        const popoverFieldNodes = {};\n        const filtersInfo = {};\n\n        visitXML(arch, (node) => {\n            switch (node.tagName) {\n                case \"calendar\": {\n                    if (!node.hasAttribute(\"date_start\")) {\n                        throw new CalendarParseArchError(\n                            `Calendar view has not defined \"date_start\" attribute.`\n                        );\n                    }\n\n                    jsClass = node.getAttribute(\"js_class\");\n\n                    for (const fieldAttrName of FIELD_ATTRIBUTE_NAMES) {\n                        if (node.hasAttribute(fieldAttrName)) {\n                            const fieldName = node.getAttribute(fieldAttrName);\n                            fieldNames.add(fieldName);\n                            fieldMapping[fieldAttrName] = fieldName;\n                        }\n                    }\n\n                    if (node.hasAttribute(\"event_limit\")) {\n                        eventLimit = evaluateExpr(node.getAttribute(\"event_limit\"));\n                        if (!Number.isInteger(eventLimit)) {\n                            throw new CalendarParseArchError(\n                                `Calendar view's event limit should be a number`\n                            );\n                        }\n                    }\n                    if (node.hasAttribute(\"scales\")) {\n                        const scalesAttr = node.getAttribute(\"scales\");\n                        scales = scalesAttr.split(\",\").filter((scale) => SCALES.includes(scale));\n                    }\n                    if (node.hasAttribute(\"mode\")) {\n                        scale = node.getAttribute(\"mode\");\n                        if (!scales.includes(scale)) {\n                            throw new CalendarParseArchError(\n                                `Calendar view cannot display mode: ${scale}`\n                            );\n                        }\n                    }\n                    if (node.hasAttribute(\"create\")) {\n                        canCreate = exprToBoolean(node.getAttribute(\"create\"), true);\n                    }\n                    if (node.hasAttribute(\"delete\")) {\n                        canDelete = exprToBoolean(node.getAttribute(\"delete\"), true);\n                    }\n                    if (node.hasAttribute(\"edit\")) {\n                        canEdit = exprToBoolean(node.getAttribute(\"edit\"), true);\n                    }\n                    if (node.hasAttribute(\"quick_create\")) {\n                        quickCreate = exprToBoolean(node.getAttribute(\"quick_create\"), true);\n                        if (quickCreate && node.hasAttribute(\"quick_create_view_id\")) {\n                            quickCreateViewId = parseInt(\n                                node.getAttribute(\"quick_create_view_id\"),\n                                10\n                            );\n                        }\n                    }\n                    if (node.hasAttribute(\"event_open_popup\")) {\n                        hasEditDialog = exprToBoolean(node.getAttribute(\"event_open_popup\"));\n                    }\n                    if (node.hasAttribute(\"show_unusual_days\")) {\n                        showUnusualDays = exprToBoolean(node.getAttribute(\"show_unusual_days\"));\n                    }\n                    if (node.hasAttribute(\"hide_date\")) {\n                        isDateHidden = exprToBoolean(node.getAttribute(\"hide_date\"));\n                    }\n                    if (node.hasAttribute(\"hide_time\")) {\n                        isTimeHidden = exprToBoolean(node.getAttribute(\"hide_time\"));\n                    }\n                    if (node.hasAttribute(\"form_view_id\")) {\n                        formViewId = parseInt(node.getAttribute(\"form_view_id\"), 10);\n                    }\n\n                    break;\n                }\n                case \"field\": {\n                    const fieldName = node.getAttribute(\"name\");\n                    fieldNames.add(fieldName);\n\n                    const fieldInfo = Field.parseFieldNode(\n                        node,\n                        models,\n                        modelName,\n                        \"calendar\",\n                        jsClass\n                    );\n                    popoverFieldNodes[fieldName] = fieldInfo;\n\n                    const field = fields[fieldName];\n                    if (!node.hasAttribute(\"invisible\") || node.hasAttribute(\"filters\")) {\n                        let filterInfo = null;\n                        if (\n                            node.hasAttribute(\"avatar_field\") ||\n                            node.hasAttribute(\"write_model\") ||\n                            node.hasAttribute(\"write_field\") ||\n                            node.hasAttribute(\"color\") ||\n                            node.hasAttribute(\"filters\")\n                        ) {\n                            filtersInfo[fieldName] = filtersInfo[fieldName] || {\n                                avatarFieldName: null,\n                                colorFieldName: null,\n                                fieldName,\n                                filterFieldName: null,\n                                label: field.string,\n                                resModel: field.relation,\n                                writeFieldName: null,\n                                writeResModel: null,\n                            };\n                            filterInfo = filtersInfo[fieldName];\n                        }\n                        if (node.hasAttribute(\"filter_field\")) {\n                            filterInfo.filterFieldName = node.getAttribute(\"filter_field\");\n                        }\n                        if (node.hasAttribute(\"avatar_field\")) {\n                            filterInfo.avatarFieldName = node.getAttribute(\"avatar_field\");\n                        }\n                        if (node.hasAttribute(\"write_model\")) {\n                            filterInfo.writeResModel = node.getAttribute(\"write_model\");\n                        }\n                        if (node.hasAttribute(\"write_field\")) {\n                            filterInfo.writeFieldName = node.getAttribute(\"write_field\");\n                        }\n                        if (node.hasAttribute(\"filters\")) {\n                            if (node.hasAttribute(\"color\")) {\n                                filterInfo.colorFieldName = node.getAttribute(\"color\");\n                            }\n                            if (node.hasAttribute(\"avatar_field\") && field.relation) {\n                                if (\n                                    field.relation.includes([\n                                        \"res.users\",\n                                        \"res.partners\",\n                                        \"hr.employee\",\n                                    ])\n                                ) {\n                                    filterInfo.avatarFieldName = \"image_128\";\n                                }\n                            }\n                        }\n                    }\n\n                    break;\n                }\n            }\n        });\n\n        return {\n            canCreate,\n            canDelete,\n            canEdit,\n            eventLimit,\n            fieldMapping,\n            fieldNames: [...fieldNames],\n            filtersInfo,\n            formViewId,\n            hasEditDialog,\n            quickCreate,\n            quickCreateViewId,\n            isDateHidden,\n            isTimeHidden,\n            popoverFieldNodes,\n            scale,\n            scales,\n            showUnusualDays,\n        };\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { is24HourFormat } from \"@web/core/l10n/dates\";\nimport { registry } from \"@web/core/registry\";\nimport { Field } from \"@web/views/fields/field\";\nimport { Record } from \"@web/model/record\";\nimport { getFormattedDateSpan } from \"@web/views/calendar/utils\";\n\nimport { Component, useExternalListener } from \"@odoo/owl\";\n\nexport class CalendarCommonPopover extends Component {\n    static template = \"web.CalendarCommonPopover\";\n    static subTemplates = {\n        popover: \"web.CalendarCommonPopover.popover\",\n        body: \"web.CalendarCommonPopover.body\",\n        footer: \"web.CalendarCommonPopover.footer\",\n    };\n    static components = {\n        Dialog,\n        Field,\n        Record,\n    };\n    static props = {\n        close: Function,\n        record: Object,\n        model: Object,\n        createRecord: Function,\n        deleteRecord: Function,\n        editRecord: Function,\n    };\n\n    setup() {\n        this.time = null;\n        this.timeDuration = null;\n        this.date = null;\n        this.dateDuration = null;\n\n        useExternalListener(window, \"pointerdown\", (e) => e.preventDefault(), { capture: true });\n\n        this.computeDateTimeAndDuration();\n    }\n\n    get activeFields() {\n        return this.props.model.activeFields;\n    }\n    get isEventEditable() {\n        return true;\n    }\n    get isEventDeletable() {\n        return this.props.model.canDelete;\n    }\n    get hasFooter() {\n        return this.isEventEditable || this.isEventDeletable;\n    }\n\n    isInvisible(fieldNode, record) {\n        return evaluateBooleanExpr(fieldNode.invisible, record.evalContextWithVirtualIds);\n    }\n\n    getFormattedValue(fieldName, record) {\n        const fieldInfo = this.props.model.popoverFieldNodes[fieldName];\n        const field = this.props.model.fields[fieldName];\n        let format;\n        const formattersRegistry = registry.category(\"formatters\");\n        if (fieldInfo.widget && formattersRegistry.contains(fieldInfo.widget)) {\n            format = formattersRegistry.get(fieldInfo.widget);\n        } else {\n            format = formattersRegistry.get(field.type);\n        }\n        return format(record.data[fieldName]);\n    }\n\n    computeDateTimeAndDuration() {\n        const record = this.props.record;\n        const { start, end } = record;\n        const isSameDay = start.hasSame(end, \"day\");\n\n        if (!record.isTimeHidden && !record.isAllDay && isSameDay) {\n            const timeFormat = is24HourFormat() ? \"HH:mm\" : \"hh:mm a\";\n            this.time = `${start.toFormat(timeFormat)} - ${end.toFormat(timeFormat)}`;\n\n            const duration = end.diff(start, [\"hours\", \"minutes\"]);\n            const formatParts = [];\n            if (duration.hours > 0) {\n                const hourString = duration.hours === 1 ? _t(\"hour\") : _t(\"hours\");\n                formatParts.push(`h '${hourString}'`);\n            }\n            if (duration.minutes > 0) {\n                const minuteStr = duration.minutes === 1 ? _t(\"minute\") : _t(\"minutes\");\n                formatParts.push(`m '${minuteStr}'`);\n            }\n            this.timeDuration = duration.toFormat(formatParts.join(\", \"));\n        }\n\n        if (!this.props.model.isDateHidden) {\n            this.date = getFormattedDateSpan(start, end);\n\n            if (record.isAllDay) {\n                if (isSameDay) {\n                    this.dateDuration = _t(\"All day\");\n                } else {\n                    const duration = end.plus({ day: 1 }).diff(start, \"days\");\n                    this.dateDuration = duration.toFormat(`d '${_t(\"days\")}'`);\n                }\n            }\n        }\n    }\n\n    onEditEvent() {\n        this.props.editRecord(this.props.record);\n        this.props.close();\n    }\n    onDeleteEvent() {\n        this.props.deleteRecord(this.props.record);\n        this.props.close();\n    }\n}\n", "import { getLocalWeekNumber, is24HourFormat } from \"@web/core/l10n/dates\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { renderToString } from \"@web/core/utils/render\";\nimport { getColor } from \"../colors\";\nimport { useCalendarPopover, useClickHandler, useFullCalendar } from \"../hooks\";\nimport { CalendarCommonPopover } from \"./calendar_common_popover\";\nimport { makeWeekColumn } from \"./calendar_common_week_column\";\n\nimport { Component } from \"@odoo/owl\";\nimport { useBus } from \"@web/core/utils/hooks\";\n\nconst SCALE_TO_FC_VIEW = {\n    day: \"timeGridDay\",\n    week: \"timeGridWeek\",\n    month: \"dayGridMonth\",\n};\nconst SCALE_TO_HEADER_FORMAT = {\n    day: \"DDD\",\n    week: \"EEE d\",\n    month: \"EEEE\",\n};\nconst SHORT_SCALE_TO_HEADER_FORMAT = {\n    ...SCALE_TO_HEADER_FORMAT,\n    day: \"D\",\n    month: \"EEE\",\n};\nconst HOUR_FORMATS = {\n    12: {\n        hour: \"numeric\",\n        minute: \"2-digit\",\n        omitZeroMinute: true,\n        meridiem: \"short\",\n    },\n    24: {\n        hour: \"numeric\",\n        minute: \"2-digit\",\n        hour12: false,\n    },\n};\n\nconst { DateTime } = luxon;\n\nexport class CalendarCommonRenderer extends Component {\n    static components = {\n        Popover: CalendarCommonPopover,\n    };\n    static template = \"web.CalendarCommonRenderer\";\n    static eventTemplate = \"web.CalendarCommonRenderer.event\";\n    static headerTemplate = \"web.CalendarCommonRendererHeader\";\n    static props = {\n        model: Object,\n        displayName: { type: String, optional: true },\n        isWeekendVisible: { type: Boolean, optional: true },\n        createRecord: Function,\n        editRecord: Function,\n        deleteRecord: Function,\n        setDate: { type: Function, optional: true },\n    };\n\n    setup() {\n        this.fc = useFullCalendar(\"fullCalendar\", this.options);\n        this.click = useClickHandler(this.onClick, this.onDblClick);\n        this.popover = useCalendarPopover(this.constructor.components.Popover);\n        useBus(this.props.model.bus, \"SCROLL_TO_CURRENT_HOUR\", () =>\n            this.fc.api.scrollToTime(`${luxon.DateTime.local().hour - 2}:00:00`)\n        );\n    }\n\n    get options() {\n        return {\n            allDaySlot: true,\n            allDayContent: \"\",\n            dayHeaderFormat: this.env.isSmall\n                ? SHORT_SCALE_TO_HEADER_FORMAT[this.props.model.scale]\n                : SCALE_TO_HEADER_FORMAT[this.props.model.scale],\n            dateClick: this.onDateClick,\n            dayCellClassNames: this.getDayCellClassNames,\n            initialDate: this.props.model.date.toISO(),\n            initialView: SCALE_TO_FC_VIEW[this.props.model.scale],\n            direction: localization.direction,\n            droppable: true,\n            editable: this.props.model.canEdit,\n            eventClick: this.onEventClick,\n            eventDragStart: this.onEventDragStart,\n            eventDrop: this.onEventDrop,\n            dayMaxEventRows: this.props.model.eventLimit,\n            moreLinkClick: this.onEventLimitClick,\n            eventMouseEnter: this.onEventMouseEnter,\n            eventMouseLeave: this.onEventMouseLeave,\n            eventClassNames: this.eventClassNames,\n            eventDidMount: this.onEventDidMount,\n            eventContent: this.onEventContent,\n            eventResizableFromStart: true,\n            eventResize: this.onEventResize,\n            eventResizeStart: this.onEventResizeStart,\n            events: (_, successCb) => successCb(this.mapRecordsToEvents()),\n            firstDay: this.props.model.firstDayOfWeek,\n            headerToolbar: false,\n            height: \"100%\",\n            locale: luxon.Settings.defaultLocale,\n            longPressDelay: 500,\n            navLinks: false,\n            nowIndicator: true,\n            select: this.onSelect,\n            selectAllow: this.isSelectionAllowed,\n            selectMinDistance: 5, // needed to not trigger select when click\n            selectMirror: true,\n            selectable: this.props.model.canCreate,\n            slotLabelFormat: is24HourFormat() ? HOUR_FORMATS[24] : HOUR_FORMATS[12],\n            snapDuration: { minutes: 15 },\n            timeZone: luxon.Settings.defaultZone.name,\n            unselectAuto: false,\n            weekNumberFormat: {\n                week: this.props.model.scale === \"month\" || this.env.isSmall ? \"numeric\" : \"long\",\n            },\n            weekends: this.props.isWeekendVisible,\n            weekNumberCalculation: (date) => getLocalWeekNumber(date),\n            weekNumbers: true,\n            dayHeaderContent: this.getHeaderHtml,\n            eventDisplay: \"block\", // Restore old render in daygrid view for single-day timed events\n            viewDidMount: this.viewDidMount,\n            moreLinkDidMount: this.wrapMoreLink,\n        };\n    }\n\n    get customOptions() {\n        return {\n            weekNumbersWithinDays: !this.env.isSmall,\n        };\n    }\n\n    viewDidMount({ el, view }) {\n        const showWeek = view.calendar.currentData.options.weekNumbers;\n        const weekText = view.calendar.currentData.options.weekText;\n        const weekColumn = !this.customOptions.weekNumbersWithinDays;\n        if (showWeek && weekColumn) {\n            makeWeekColumn({ el, weekText });\n        }\n    }\n\n    getStartTime(record) {\n        const timeFormat = is24HourFormat() ? \"HH:mm\" : \"hh:mm a\";\n        return record.start.toFormat(timeFormat);\n    }\n\n    getEndTime(record) {\n        const timeFormat = is24HourFormat() ? \"HH:mm\" : \"hh:mm a\";\n        return record.end.toFormat(timeFormat);\n    }\n\n    computeEventSelector(event) {\n        return `[data-event-id=\"${event.id}\"]`;\n    }\n    highlightEvent(event, className) {\n        for (const el of this.fc.el.querySelectorAll(this.computeEventSelector(event))) {\n            el.classList.add(className);\n        }\n    }\n    unhighlightEvent(event, className) {\n        for (const el of this.fc.el.querySelectorAll(this.computeEventSelector(event))) {\n            el.classList.remove(className);\n        }\n    }\n    mapRecordsToEvents() {\n        return Object.values(this.props.model.records).map((r) => this.convertRecordToEvent(r));\n    }\n    convertRecordToEvent(record) {\n        const allDay = record.isAllDay || record.end.diff(record.start, \"hours\").hours >= 24;\n        return {\n            id: record.id,\n            title: record.title,\n            start: record.start.toISO(),\n            end:\n                [\"week\", \"month\"].includes(this.props.model.scale) && allDay ||\n                (record.isAllDay ||\n                    (allDay && record.end.toMillis() !== record.end.startOf(\"day\").toMillis())\n                )\n                    ? record.end.plus({ days: 1 }).toISO()\n                    : record.end.toISO(),\n            allDay: allDay,\n        };\n    }\n    getPopoverProps(record) {\n        return {\n            record,\n            model: this.props.model,\n            createRecord: this.props.createRecord,\n            deleteRecord: this.props.deleteRecord,\n            editRecord: this.props.editRecord,\n        };\n    }\n    openPopover(target, record) {\n        const color = getColor(record.colorIndex);\n        this.popover.open(\n            target,\n            this.getPopoverProps(record),\n            `o_cw_popover card o_calendar_color_${typeof color === \"number\" ? color : 0}`\n        );\n    }\n\n    onClick(info) {\n        this.openPopover(info.el, this.props.model.records[info.event.id]);\n        this.highlightEvent(info.event, \"o_cw_custom_highlight\");\n    }\n    onDateClick(info) {\n        if (info.jsEvent.defaultPrevented) {\n            return;\n        }\n        this.props.createRecord(this.fcEventToRecord(info));\n    }\n    getDayCellClassNames(info) {\n        const date = luxon.DateTime.fromJSDate(info.date).toISODate();\n        if (this.props.model.unusualDays.includes(date)) {\n            return [\"o_calendar_disabled\"];\n        }\n        return [];\n    }\n    onDblClick(info) {\n        this.props.editRecord(this.props.model.records[info.event.id]);\n    }\n    onEventClick(info) {\n        this.click(info);\n    }\n    onEventContent({ event }) {\n        const record = this.props.model.records[event.id];\n        if (record) {\n            // This is needed in order to give the possibility to change the event template.\n            const injectedContentStr = renderToString(this.constructor.eventTemplate, {\n                ...record,\n                startTime: this.getStartTime(record),\n                endTime: this.getEndTime(record),\n            });\n            const domParser = new DOMParser();\n            const { children } = domParser.parseFromString(injectedContentStr, \"text/html\").body;\n            return { domNodes: children };\n        }\n        return true;\n    }\n    eventClassNames({ el, event }) {\n        const classesToAdd = [];\n        classesToAdd.push(\"o_event\");\n        const record = this.props.model.records[event.id];\n\n        if (record) {\n            const color = getColor(record.colorIndex);\n            if (typeof color === \"number\") {\n                classesToAdd.push(`o_calendar_color_${color}`);\n            } else if (typeof color !== \"string\") {\n                classesToAdd.push(\"o_calendar_color_0\");\n            }\n\n            if (record.isHatched) {\n                classesToAdd.push(\"o_event_hatched\");\n            }\n            if (record.isStriked) {\n                classesToAdd.push(\"o_event_striked\");\n            }\n            if (record.duration <= 0.25) {\n                classesToAdd.push(\"o_event_oneliner\");\n            }\n            if (DateTime.now() >= record.end) {\n                classesToAdd.push(\"o_past_event\");\n            }\n\n            if (!record.isAllDay && !record.isTimeHidden && record.isMonth) {\n                classesToAdd.push(\"o_event_dot\");\n            } else if (record.isAllDay) {\n                classesToAdd.push(\"o_event_allday\");\n            }\n        }\n        return classesToAdd;\n    }\n    onEventDidMount({ el, event }) {\n        el.dataset.eventId = event.id;\n        const record = this.props.model.records[event.id];\n\n        if (record) {\n            if (record.isMonth) {\n                el.querySelector(\".fc-event-main\").classList.add(\n                    \"d-flex\",\n                    \"gap-1\",\n                    \"text-truncate\"\n                );\n            }\n            const color = getColor(record.colorIndex);\n            if (typeof color === \"string\") {\n                el.style.backgroundColor = color;\n            }\n\n            if (!el.classList.contains(\"fc-bg\")) {\n                const bg = document.createElement(\"div\");\n                bg.classList.add(\"fc-bg\");\n                el.appendChild(bg);\n            }\n        }\n    }\n    async onSelect(info) {\n        info.jsEvent.preventDefault();\n        this.popover.close();\n        await this.props.createRecord(this.fcEventToRecord(info));\n        this.fc.api.unselect();\n    }\n    isSelectionAllowed(event) {\n        return event.end.getDate() === event.start.getDate() || event.allDay;\n    }\n    onEventDrop(info) {\n        this.fc.api.unselect();\n        this.props.model.updateRecord(this.fcEventToRecord(info.event), { moved: true });\n    }\n    onEventResize(info) {\n        this.fc.api.unselect();\n        this.props.model.updateRecord(this.fcEventToRecord(info.event));\n    }\n    fcEventToRecord(event) {\n        const { id, allDay, date, start, end } = event;\n        const res = {\n            start: luxon.DateTime.fromJSDate(date || start),\n            isAllDay: allDay,\n        };\n        if (end) {\n            res.end = luxon.DateTime.fromJSDate(end);\n            if ([\"week\", \"month\"].includes(this.props.model.scale) && allDay) {\n                res.end = res.end.minus({ days: 1 });\n            }\n        }\n        if (id) {\n            const existingRecord = this.props.model.records[id];\n            if (this.props.model.scale === \"month\") {\n                res.start = res.start?.set({\n                    hour: existingRecord.start.hour,\n                    minute: existingRecord.start.minute,\n                });\n                if (existingRecord.end) {\n                    res.end = res.end?.set({\n                        hour: existingRecord.end.hour,\n                        minute: existingRecord.end.minute,\n                    });\n                }\n            }\n            res.id = existingRecord.id;\n        }\n        return res;\n    }\n    onEventMouseEnter(info) {\n        this.highlightEvent(info.event, \"o_cw_custom_highlight\");\n    }\n    onEventMouseLeave(info) {\n        if (!info.event.id) {\n            return;\n        }\n        this.unhighlightEvent(info.event, \"o_cw_custom_highlight\");\n    }\n    onEventDragStart(info) {\n        info.el.classList.add(info.view.type);\n        this.fc.api.unselect();\n        this.highlightEvent(info.event, \"o_cw_custom_highlight\");\n    }\n    onEventResizeStart(info) {\n        this.fc.api.unselect();\n        this.highlightEvent(info.event, \"o_cw_custom_highlight\");\n    }\n    onEventLimitClick() {\n        this.fc.api.unselect();\n        return \"popover\";\n    }\n    onWindowResize() {\n        this.updateSize();\n    }\n\n    getHeaderHtml({ date }) {\n        return {\n            html: renderToString(this.constructor.headerTemplate, this.headerTemplateProps(date)),\n        };\n    }\n\n    headerTemplateProps(date) {\n        const scale = this.props.model.scale;\n        // when rendering months, FullCalendar uses a date w/out tz\n        // so use UTC instead of local tz when converting to DateTime\n        const options = scale === \"month\" ? { zone: \"UTC\" } : {};\n        const { weekdayShort, weekdayLong, day } = DateTime.fromJSDate(date, options);\n        return {\n            weekdayShort,\n            weekdayLong,\n            day,\n            scale,\n        };\n    }\n\n    wrapMoreLink({ el }) {\n        const wrapper = document.createElement(\"div\");\n        wrapper.classList.add(\"fc-more-cell\");\n        el.classList.remove(\"fc-daygrid-more-link\");\n        el.parentNode.insertBefore(wrapper, el);\n        wrapper.appendChild(el);\n    }\n}\n", "export function makeWeekColumn({ el, showWeek, weekColumn, weekText }) {\n    const firstRows = el.querySelectorAll(\".fc-col-header-cell:nth-child(1), .fc-day:nth-child(1)\");\n    for (const element of firstRows) {\n        const newElement = document.createElement(\"th\");\n        if (element.classList.contains(\"fc-col-header-cell\")) {\n            newElement.classList.add(\"o-fc-week-header\");\n            newElement.innerText = weekText;\n        } else {\n            newElement.classList.add(\"o-fc-week\");\n            const weekElement = element.querySelector(\".fc-daygrid-week-number\");\n            weekElement.classList.remove(\"fc-daygrid-week-number\");\n            newElement.append(weekElement);\n        }\n        element.parentElement.insertBefore(newElement, element);\n    }\n}\n", "import {\n    deleteConfirmationMessage,\n    ConfirmationDialog,\n} from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useOwnedDialogs, useService } from \"@web/core/utils/hooks\";\nimport { Layout } from \"@web/search/layout\";\nimport { useModelWithSampleData } from \"@web/model/model\";\nimport { FormViewDialog } from \"@web/views/view_dialogs/form_view_dialog\";\nimport { useSetupAction } from \"@web/search/action_hook\";\nimport { DateTimePicker } from \"@web/core/datetime/datetime_picker\";\nimport { CalendarFilterPanel } from \"./filter_panel/calendar_filter_panel\";\nimport { CalendarMobileFilterPanel } from \"./mobile_filter_panel/calendar_mobile_filter_panel\";\nimport { CalendarQuickCreate } from \"./quick_create/calendar_quick_create\";\nimport { SearchBar } from \"@web/search/search_bar/search_bar\";\nimport { useSearchBarToggler } from \"@web/search/search_bar/search_bar_toggler\";\nimport { ViewScaleSelector } from \"@web/views/view_components/view_scale_selector\";\nimport { CogMenu } from \"@web/search/cog_menu/cog_menu\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { standardViewProps } from \"@web/views/standard_view_props\";\nimport { getLocalWeekNumber } from \"@web/core/l10n/dates\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nconst { DateTime } = luxon;\n\nexport const SCALE_LABELS = {\n    day: _t(\"Day\"),\n    week: _t(\"Week\"),\n    month: _t(\"Month\"),\n    year: _t(\"Year\"),\n};\n\nfunction useUniqueDialog() {\n    const displayDialog = useOwnedDialogs();\n    let close = null;\n    return (...args) => {\n        if (close) {\n            close();\n        }\n        close = displayDialog(...args);\n    };\n}\n\nexport class CalendarController extends Component {\n    static components = {\n        DatePicker: DateTimePicker,\n        FilterPanel: CalendarFilterPanel,\n        MobileFilterPanel: CalendarMobileFilterPanel,\n        QuickCreate: CalendarQuickCreate,\n        QuickCreateFormView: FormViewDialog,\n        Layout,\n        SearchBar,\n        ViewScaleSelector,\n        CogMenu,\n    };\n    static template = \"web.CalendarController\";\n    static props = {\n        ...standardViewProps,\n        Model: Function,\n        Renderer: Function,\n        archInfo: Object,\n        buttonTemplate: String,\n        session: { type: Object, optional: true },\n        itemCalendarProps: { type: Object, optional: true },\n    };\n\n    setup() {\n        this.action = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.displayDialog = useUniqueDialog();\n\n        this.model = useModelWithSampleData(\n            this.props.Model,\n            {\n                ...this.props.archInfo,\n                resModel: this.props.resModel,\n                domain: this.props.domain,\n                fields: this.props.fields,\n                allFilter: this.props.state?.allFilter ?? {},\n                date: this.props.state?.date,\n            },\n            {\n                onWillStart: this.onWillStartModel.bind(this),\n            }\n        );\n\n        useSetupAction({\n            getLocalState: () => this.model.exportedState,\n        });\n\n        const sessionShowSidebar = browser.sessionStorage.getItem(\"calendar.showSideBar\");\n        this.state = useState({\n            isWeekendVisible:\n                browser.localStorage.getItem(\"calendar.isWeekendVisible\") != null\n                    ? JSON.parse(browser.localStorage.getItem(\"calendar.isWeekendVisible\"))\n                    : true,\n            showSideBar:\n                !this.env.isSmall &&\n                Boolean(sessionShowSidebar != null ? JSON.parse(sessionShowSidebar) : true),\n        });\n\n        this.searchBarToggler = useSearchBarToggler();\n    }\n\n    get currentDate() {\n        const meta = this.model.meta;\n        const scale = meta.scale;\n        if (this.env.isSmall && [\"week\", \"month\"].includes(scale)) {\n            const date = meta.date || DateTime.now();\n            let text = \"\";\n            if (scale === \"week\") {\n                const startMonth = date.startOf(\"week\");\n                const endMonth = date.endOf(\"week\");\n                if (startMonth.toFormat(\"LLL\") !== endMonth.toFormat(\"LLL\")) {\n                    text = `${startMonth.toFormat(\"LLL\")}-${endMonth.toFormat(\"LLL\")}`;\n                } else {\n                    text = startMonth.toFormat(\"LLLL\");\n                }\n            } else if (scale === \"month\") {\n                text = date.toFormat(\"LLLL\");\n            }\n            return ` - ${text} ${date.year}`;\n        } else {\n            return \"\";\n        }\n    }\n\n    get date() {\n        return this.model.meta.date || DateTime.now();\n    }\n\n    get today() {\n        return DateTime.now().toFormat(\"d\");\n    }\n\n    get currentYear() {\n        return this.date.toFormat(\"y\");\n    }\n\n    get dayHeader() {\n        return `${this.date.toFormat(\"d\")} ${this.date.toFormat(\"MMMM\")} ${this.date.year}`;\n    }\n\n    get weekHeader() {\n        const { rangeStart, rangeEnd } = this.model;\n        if (rangeStart.year != rangeEnd.year) {\n            return `${rangeStart.toFormat(\"MMMM\")} ${rangeStart.year} - ${rangeEnd.toFormat(\n                \"MMMM\"\n            )} ${rangeEnd.year}`;\n        } else if (rangeStart.month != rangeEnd.month) {\n            return `${rangeStart.toFormat(\"MMMM\")} - ${rangeEnd.toFormat(\"MMMM\")} ${\n                rangeStart.year\n            }`;\n        }\n        return `${rangeStart.toFormat(\"MMMM\")} ${rangeStart.year}`;\n    }\n\n    get currentMonth() {\n        return `${this.date.toFormat(\"MMMM\")} ${this.date.year}`;\n    }\n\n    get currentWeek() {\n        return getLocalWeekNumber(this.model.rangeStart);\n    }\n\n    get rendererProps() {\n        return {\n            model: this.model,\n            isWeekendVisible: this.model.scale === \"day\" || this.state.isWeekendVisible,\n            createRecord: this.createRecord.bind(this),\n            deleteRecord: this.deleteRecord.bind(this),\n            editRecord: this.editRecord.bind(this),\n            setDate: this.setDate.bind(this),\n        };\n    }\n    get containerProps() {\n        return {\n            model: this.model,\n        };\n    }\n    get datePickerProps() {\n        return {\n            type: \"date\",\n            showWeekNumbers: false,\n            maxPrecision: \"days\",\n            daysOfWeekFormat: \"narrow\",\n            onSelect: (date) => {\n                let scale = \"week\";\n\n                if (this.model.date.hasSame(date, \"day\")) {\n                    const scales = [\"month\", \"week\", \"day\"];\n                    scale = scales[(scales.indexOf(this.model.scale) + 1) % scales.length];\n                } else {\n                    // Check if dates are on the same week\n                    // As a.hasSame(b, \"week\") does not depend on locale and week always starts on Monday,\n                    // we are comparing derivated dates instead to take this into account.\n                    const currentDate =\n                        this.model.date.weekday === 7\n                            ? this.model.date.plus({ day: 1 })\n                            : this.model.date;\n                    const pickedDate = date.weekday === 7 ? date.plus({ day: 1 }) : date;\n\n                    // a.hasSame(b, \"week\") does not depend on locale and week alway starts on Monday\n                    if (currentDate.hasSame(pickedDate, \"week\")) {\n                        scale = \"day\";\n                    }\n                }\n\n                this.model.load({ scale, date });\n            },\n            value: this.model.date,\n        };\n    }\n    get filterPanelProps() {\n        return {\n            model: this.model,\n        };\n    }\n    get mobileFilterPanelProps() {\n        return {\n            model: this.model,\n            sideBarShown: this.state.showSideBar,\n            toggleSideBar: () => {\n                this.state.showSideBar = !this.state.showSideBar;\n            },\n        };\n    }\n\n    toggleSideBar() {\n        this.state.showSideBar = !this.state.showSideBar;\n        browser.sessionStorage.setItem(\"calendar.showSideBar\", this.state.showSideBar);\n    }\n\n    get showCalendar() {\n        return !this.env.isSmall || !this.state.showSideBar;\n    }\n\n    get showSideBar() {\n        return this.state.showSideBar;\n    }\n\n    get className() {\n        return this.props.className;\n    }\n\n    get editRecordDefaultDisplayText() {\n        return _t(\"New Event\");\n    }\n\n    getQuickCreateProps(record) {\n        return {\n            record,\n            model: this.model,\n            editRecord: this.editRecordInCreation.bind(this),\n            title: this.props.context.default_name,\n        };\n    }\n\n    getQuickCreateFormViewProps(record) {\n        const rawRecord = this.model.buildRawRecord(record);\n        const context = this.model.makeContextDefaults(rawRecord);\n        return {\n            resModel: this.model.resModel,\n            viewId: this.model.quickCreateFormViewId,\n            title: _t(\"New Event\"),\n            context,\n        };\n    }\n\n    createRecord(record) {\n        if (!this.model.canCreate) {\n            return;\n        }\n        if (this.model.hasQuickCreate) {\n            if (this.model.quickCreateFormViewId) {\n                return new Promise((resolve) => {\n                    this.displayDialog(\n                        this.constructor.components.QuickCreateFormView,\n                        this.getQuickCreateFormViewProps(record),\n                        {\n                            onClose: () => resolve(),\n                        }\n                    );\n                });\n            }\n\n            return new Promise((resolve) => {\n                this.displayDialog(\n                    this.constructor.components.QuickCreate,\n                    this.getQuickCreateProps(record),\n                    {\n                        onClose: () => resolve(),\n                    }\n                );\n            });\n        } else {\n            return this.editRecordInCreation(record);\n        }\n    }\n    async editRecord(record, context = {}, shouldFetchFormViewId = true) {\n        if (this.model.hasEditDialog) {\n            return new Promise((resolve) => {\n                this.displayDialog(\n                    FormViewDialog,\n                    {\n                        resModel: this.model.resModel,\n                        resId: record.id || false,\n                        context,\n                        title: record.id\n                            ? _t(\"Open: %s\", record.title)\n                            : this.editRecordDefaultDisplayText,\n                        viewId: this.model.formViewId,\n                        onRecordSaved: () => this.model.load(),\n                    },\n                    { onClose: () => resolve() }\n                );\n            });\n        } else {\n            let formViewId = this.model.formViewId;\n            if (shouldFetchFormViewId) {\n                formViewId = await this.orm.call(\n                    this.model.resModel,\n                    \"get_formview_id\",\n                    [[record.id]],\n                    context\n                );\n            }\n            const action = {\n                type: \"ir.actions.act_window\",\n                res_model: this.model.resModel,\n                views: [[formViewId || false, \"form\"]],\n                target: \"current\",\n                context,\n            };\n            if (record.id) {\n                action.res_id = record.id;\n            }\n            this.action.doAction(action);\n        }\n    }\n    editRecordInCreation(record) {\n        const rawRecord = this.model.buildRawRecord(record);\n        const context = this.model.makeContextDefaults(rawRecord);\n        return this.editRecord(record, context, false);\n    }\n\n    deleteConfirmationDialogProps(record) {\n        return {\n            title: _t(\"Bye-bye, record!\"),\n            body: deleteConfirmationMessage,\n            confirm: () => {\n                this.model.unlinkRecord(record.id);\n            },\n            confirmLabel: _t(\"Delete\"),\n            cancel: () => {\n                // `ConfirmationDialog` needs this prop to display the cancel\n                // button but we do nothing on cancel.\n            },\n            cancelLabel: _t(\"No, keep it\"),\n        };\n    }\n\n    deleteRecord(record) {\n        this.displayDialog(ConfirmationDialog, this.deleteConfirmationDialogProps(record));\n    }\n\n    onWillStartModel() {}\n\n    async setDate(move) {\n        let date = null;\n        switch (move) {\n            case \"next\":\n                date = this.model.date.plus({ [`${this.model.scale}s`]: 1 });\n                break;\n            case \"previous\":\n                date = this.model.date.minus({ [`${this.model.scale}s`]: 1 });\n                break;\n            case \"today\":\n                date = luxon.DateTime.local().startOf(\"day\");\n                if (date.ts === this.date.startOf(\"day\").ts) {\n                    this.model.bus.trigger(\"SCROLL_TO_CURRENT_HOUR\", false);\n                }\n                break;\n        }\n        await this.model.load({ date });\n    }\n\n    get scales() {\n        return Object.fromEntries(\n            this.model.scales.map((s) => [s, { description: SCALE_LABELS[s] }])\n        );\n    }\n\n    async setScale(scale) {\n        await this.model.load({ scale });\n        browser.sessionStorage.setItem(\"calendar-scale\", this.model.scale);\n    }\n\n    toggleWeekendVisibility() {\n        this.state.isWeekendVisible = !this.state.isWeekendVisible;\n        browser.localStorage.setItem(\"calendar.isWeekendVisible\", this.state.isWeekendVisible);\n    }\n}\n", "import {\n    serializeDate,\n    serializeDateTime,\n    deserializeDate,\n    deserializeDateTime,\n} from \"@web/core/l10n/dates\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { Model } from \"@web/model/model\";\nimport { extractFieldsFromArchInfo } from \"@web/model/relational_model/utils\";\nimport { browser } from \"@web/core/browser/browser\";\n\nexport class CalendarModel extends Model {\n    setup(params, services) {\n        /** @protected */\n        this.keepLast = new KeepLast();\n\n        const formViewFromConfig = (this.env.config.views || []).find((view) => view[1] === \"form\");\n        const formViewIdFromConfig = formViewFromConfig ? formViewFromConfig[0] : false;\n        const fieldNodes = params.popoverFieldNodes;\n        const { activeFields, fields } = extractFieldsFromArchInfo({ fieldNodes }, params.fields);\n        this.meta = {\n            ...params,\n            activeFields,\n            fields,\n            firstDayOfWeek: (localization.weekStart || 0) % 7,\n            formViewId: params.formViewId || formViewIdFromConfig,\n        };\n        this.meta.scale = this.getLocalStorageScale();\n        this.data = {\n            filters: {},\n            filterSections: {},\n            hasCreateRight: null,\n            range: null,\n            records: {},\n            unusualDays: [],\n        };\n    }\n    async load(params = {}) {\n        Object.assign(this.meta, params);\n        if (!this.meta.date) {\n            this.meta.date =\n                params.context && params.context.initial_date\n                    ? deserializeDateTime(params.context.initial_date).startOf(\"day\")\n                    : luxon.DateTime.local().startOf(\"day\");\n        }\n        // Prevent picking a scale that is not supported by the view\n        if (!this.meta.scales.includes(this.meta.scale)) {\n            this.meta.scale = this.meta.scales[0];\n        }\n        browser.localStorage.setItem(this.storageKey, this.meta.scale);\n        const data = { ...this.data };\n        await this.keepLast.add(this.updateData(data));\n        this.data = data;\n        this.notify();\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    get date() {\n        return this.meta.date;\n    }\n    get canCreate() {\n        return this.meta.canCreate && this.data.hasCreateRight;\n    }\n    get canDelete() {\n        return this.meta.canDelete;\n    }\n    get canEdit() {\n        return this.meta.canEdit && !this.meta.fields[this.meta.fieldMapping.date_start].readonly;\n    }\n    get eventLimit() {\n        return this.meta.eventLimit;\n    }\n    get exportedState() {\n        return this.meta;\n    }\n    get fieldMapping() {\n        return this.meta.fieldMapping;\n    }\n    get fields() {\n        return this.meta.fields;\n    }\n    get filterSections() {\n        return Object.values(this.data.filterSections);\n    }\n    get firstDayOfWeek() {\n        return this.meta.firstDayOfWeek;\n    }\n    get formViewId() {\n        return this.meta.formViewId;\n    }\n    get hasAllDaySlot() {\n        return (\n            this.meta.fieldMapping.all_day ||\n            this.meta.fields[this.meta.fieldMapping.date_start].type === \"date\"\n        );\n    }\n    get hasEditDialog() {\n        return this.meta.hasEditDialog;\n    }\n    get hasQuickCreate() {\n        return this.meta.quickCreate;\n    }\n    get isDateHidden() {\n        return this.meta.isDateHidden;\n    }\n    get isTimeHidden() {\n        return this.meta.isTimeHidden;\n    }\n    get popoverFieldNodes() {\n        return this.meta.popoverFieldNodes;\n    }\n    get activeFields() {\n        return this.meta.activeFields;\n    }\n    get rangeEnd() {\n        return this.data.range.end;\n    }\n    get rangeStart() {\n        return this.data.range.start;\n    }\n    get records() {\n        return this.data.records;\n    }\n    get resModel() {\n        return this.meta.resModel;\n    }\n    get scale() {\n        return this.meta.scale;\n    }\n    get scales() {\n        return this.meta.scales;\n    }\n    get storageKey() {\n        return `scaleOf-viewId-${this.env.config.viewId}`;\n    }\n    get unusualDays() {\n        return this.data.unusualDays;\n    }\n\n    get quickCreateFormViewId() {\n        return this.meta.quickCreateViewId;\n    }\n\n    get defaultFilterLabel() {\n        return _t(\"Undefined\");\n    }\n\n    //--------------------------------------------------------------------------\n\n    async createFilter(fieldName, filterValue) {\n        const info = this.meta.filtersInfo[fieldName];\n        if (!info || !info.writeFieldName || !info.writeResModel) {\n            return;\n        }\n\n        const normalizedFilterValue = Array.isArray(filterValue) ? filterValue : [filterValue];\n        const dataArray = normalizedFilterValue.map((value) => {\n            const data = {\n                user_id: user.userId,\n                [info.writeFieldName]: value,\n            };\n            if (info.filterFieldName) {\n                data[info.filterFieldName] = true;\n            }\n            return data;\n        });\n\n        await this.orm.create(info.writeResModel, dataArray);\n        await this.load();\n    }\n    async createRecord(record) {\n        const rawRecord = this.buildRawRecord(record);\n        const context = this.makeContextDefaults(rawRecord);\n        await this.orm.create(this.meta.resModel, [rawRecord], { context });\n        await this.load();\n    }\n    async unlinkFilter(fieldName, recordId) {\n        const info = this.meta.filtersInfo[fieldName];\n        if (info && info.writeResModel) {\n            await this.orm.unlink(info.writeResModel, [recordId]);\n            await this.load();\n        }\n    }\n    async unlinkRecord(recordId) {\n        await this.orm.unlink(this.meta.resModel, [recordId]);\n        await this.load();\n    }\n    async updateFilters(fieldName, filters) {\n        const section = this.data.filterSections[fieldName];\n        if (section) {\n            for (const value in filters) {\n                const active = filters[value];\n                const filter = section.filters.find((filter) => `${filter.value}` === value);\n                if (filter) {\n                    filter.active = active;\n                    const info = this.meta.filtersInfo[fieldName];\n                    if (\n                        filter.recordId &&\n                        info &&\n                        info.writeFieldName &&\n                        info.writeResModel &&\n                        info.filterFieldName\n                    ) {\n                        const data = {\n                            [info.filterFieldName]: active,\n                        };\n                        await this.orm.write(info.writeResModel, [filter.recordId], data);\n                    } else if (filter.type === \"all\") {\n                        this.meta.allFilter[section.label] = active;\n                    }\n                }\n            }\n        }\n        await this.load();\n    }\n    async updateRecord(record, options = {}) {\n        const rawRecord = this.buildRawRecord(record, options);\n        delete rawRecord.name; // name is immutable.\n        await this.orm.write(this.meta.resModel, [record.id], rawRecord, {\n            context: { from_ui: true },\n        });\n        await this.load();\n    }\n\n    //--------------------------------------------------------------------------\n\n    buildRawRecord(partialRecord, options = {}) {\n        const data = {};\n        data[this.meta.fieldMapping.create_name_field || \"name\"] = partialRecord.title;\n\n        let start = partialRecord.start;\n        let end = partialRecord.end;\n\n        if (!end || !end.isValid) {\n            // Set end date if not existing\n            if (partialRecord.isAllDay) {\n                end = start;\n            } else {\n                // in week mode or day mode, convert allday event to event\n                end = start.plus({ hours: options.duration_hour || 1 });\n            }\n        }\n\n        const isDateEvent = this.fields[this.meta.fieldMapping.date_start].type === \"date\";\n        // An \"all day\" event without the \"all_day\" option is not considered\n        // as a 24h day. It's just a part of the day (by default: 7h-19h).\n        if (partialRecord.isAllDay) {\n            if (!this.hasAllDaySlot && !isDateEvent && !partialRecord.id) {\n                // default hours in the user's timezone\n                start = start.set({ hours: 7 });\n                end = end.set({ hours: 19 });\n            }\n        }\n\n        if (this.meta.fieldMapping.all_day) {\n            data[this.meta.fieldMapping.all_day] = partialRecord.isAllDay;\n        }\n\n        data[this.meta.fieldMapping.date_start] =\n            (partialRecord.isAllDay && this.hasAllDaySlot\n                ? \"date\"\n                : this.fields[this.meta.fieldMapping.date_start].type) === \"date\"\n                ? serializeDate(start)\n                : serializeDateTime(start);\n\n        if (this.meta.fieldMapping.date_stop) {\n            data[this.meta.fieldMapping.date_stop] =\n                (partialRecord.isAllDay && this.hasAllDaySlot\n                    ? \"date\"\n                    : this.fields[this.meta.fieldMapping.date_start].type) === \"date\"\n                    ? serializeDate(end)\n                    : serializeDateTime(end);\n        }\n\n        if (this.meta.fieldMapping.date_delay) {\n            if (this.meta.scale !== \"month\" || !options.moved) {\n                data[this.meta.fieldMapping.date_delay] = end.diff(start, \"hours\").hours;\n            }\n        }\n        return data;\n    }\n    makeContextDefaults(rawRecord) {\n        const { fieldMapping, scale } = this.meta;\n\n        const context = { ...this.meta.context };\n        const fieldNames = [\n            fieldMapping.create_name_field || \"name\",\n            fieldMapping.date_start,\n            fieldMapping.date_stop,\n            fieldMapping.date_delay,\n            fieldMapping.all_day || \"allday\",\n        ];\n        for (const fieldName of fieldNames) {\n            // fieldName could be in rawRecord but not defined\n            if (rawRecord[fieldName] !== undefined) {\n                context[`default_${fieldName}`] = rawRecord[fieldName];\n            }\n        }\n        if ([\"month\", \"year\"].includes(scale)) {\n            context[`default_${fieldMapping.all_day || \"allday\"}`] = true;\n        }\n\n        return context;\n    }\n\n    //--------------------------------------------------------------------------\n    // Protected\n    //--------------------------------------------------------------------------\n\n    /**\n     * @protected\n     */\n    async updateData(data) {\n        if (data.hasCreateRight === null) {\n            data.hasCreateRight = await user.checkAccessRight(this.meta.resModel, \"create\");\n        }\n        data.range = this.computeRange();\n        if (this.meta.showUnusualDays) {\n            data.unusualDays = await this.loadUnusualDays(data);\n        }\n\n        const { sections, dynamicFiltersInfo } = await this.loadFilters(data);\n\n        // Load records and dynamic filters only with fresh filters\n        data.filterSections = sections;\n        data.records = await this.loadRecords(data);\n        const dynamicSections = await this.loadDynamicFilters(data, dynamicFiltersInfo);\n\n        // Apply newly computed filter sections\n        Object.assign(data.filterSections, dynamicSections);\n\n        // Remove records that don't match dynamic filters\n        for (const [recordId, record] of Object.entries(data.records)) {\n            for (const [fieldName, filterInfo] of Object.entries(dynamicSections)) {\n                for (const filter of filterInfo.filters) {\n                    const rawValue = record.rawRecord[fieldName];\n                    const value = Array.isArray(rawValue) ? rawValue[0] : rawValue;\n                    if (filter.value === value && !filter.active) {\n                        delete data.records[recordId];\n                    }\n                }\n            }\n        }\n    }\n\n    //--------------------------------------------------------------------------\n\n    /**\n     * @protected\n     */\n    computeRange() {\n        const { scale, date, firstDayOfWeek } = this.meta;\n        let start = date;\n        let end = date;\n\n        if (scale !== \"week\") {\n            // startOf(\"week\") does not depend on locale and will always give the\n            // \"Monday\" of the week...\n            start = start.startOf(scale);\n            end = end.endOf(scale);\n        }\n\n        if ([\"week\", \"month\"].includes(scale)) {\n            const currentWeekOffset = (start.weekday - firstDayOfWeek + 7) % 7;\n            start = start.minus({ days: currentWeekOffset });\n            end = start.plus({ weeks: scale === \"week\" ? 1 : 6, days: -1 });\n        }\n\n        start = start.startOf(\"day\");\n        end = end.endOf(\"day\");\n\n        return { start, end };\n    }\n\n    //--------------------------------------------------------------------------\n\n    /**\n     * @protected\n     */\n    computeDomain(data) {\n        return [\n            ...this.meta.domain,\n            ...this.computeRangeDomain(data),\n            ...this.computeFiltersDomain(data),\n        ];\n    }\n    /**\n     * @protected\n     */\n    computeFiltersDomain(data) {\n        // List authorized values for every field\n        // fields with an active \"all\" filter are skipped\n        const authorizedValues = {};\n        const avoidValues = {};\n\n        for (const [fieldName, filterSection] of Object.entries(data.filterSections)) {\n            // Skip \"all\" filters because they do not affect the domain\n            const filterAll = filterSection.filters.find((f) => f.type === \"all\");\n            if (!(filterAll && filterAll.active)) {\n                const filterSectionInfo = this.meta.filtersInfo[fieldName];\n\n                // Loop over subfilters to complete authorizedValues\n                for (const filter of filterSection.filters) {\n                    if (filterSectionInfo.writeResModel) {\n                        if (!authorizedValues[fieldName]) {\n                            authorizedValues[fieldName] = [];\n                        }\n                        if (filter.active) {\n                            authorizedValues[fieldName].push(filter.value);\n                        }\n                    } else {\n                        if (!filter.active) {\n                            if (!avoidValues[fieldName]) {\n                                avoidValues[fieldName] = [];\n                            }\n                            avoidValues[fieldName].push(filter.value);\n                        }\n                    }\n                }\n            }\n        }\n\n        // Compute the domain\n        const domain = [];\n        for (const field in authorizedValues) {\n            domain.push([field, \"in\", authorizedValues[field]]);\n        }\n        for (const field in avoidValues) {\n            if (avoidValues[field].length > 0) {\n                domain.push([field, \"not in\", avoidValues[field]]);\n            }\n        }\n        return domain;\n    }\n    /**\n     * @protected\n     */\n    computeRangeDomain(data) {\n        const { fieldMapping } = this.meta;\n        const formattedEnd = serializeDateTime(data.range.end);\n        const formattedStart = serializeDateTime(data.range.start);\n\n        const domain = [[fieldMapping.date_start, \"<=\", formattedEnd]];\n        if (fieldMapping.date_stop) {\n            domain.push([fieldMapping.date_stop, \">=\", formattedStart]);\n        } else if (!fieldMapping.date_delay) {\n            domain.push([fieldMapping.date_start, \">=\", formattedStart]);\n        }\n        return domain;\n    }\n\n    //--------------------------------------------------------------------------\n\n    /**\n     * @protected\n     */\n    fetchUnusualDays(data) {\n        return this.orm.call(this.meta.resModel, \"get_unusual_days\", [\n            serializeDateTime(data.range.start),\n            serializeDateTime(data.range.end),\n        ]);\n    }\n    /**\n     * @protected\n     */\n    async loadUnusualDays(data) {\n        const unusualDays = await this.fetchUnusualDays(data);\n        return Object.entries(unusualDays)\n            .filter((entry) => entry[1])\n            .map((entry) => entry[0]);\n    }\n\n    //--------------------------------------------------------------------------\n\n    /**\n     * @protected\n     */\n    fetchRecords(data) {\n        const { fieldNames, resModel } = this.meta;\n        return this.orm.searchRead(resModel, this.computeDomain(data), [\n            ...new Set([...fieldNames, ...Object.keys(this.meta.activeFields)]),\n        ]);\n    }\n    /**\n     * @protected\n     */\n    async loadRecords(data) {\n        const rawRecords = await this.fetchRecords(data);\n        const records = {};\n        for (const rawRecord of rawRecords) {\n            records[rawRecord.id] = this.normalizeRecord(rawRecord);\n        }\n        return records;\n    }\n    /**\n     * @protected\n     * @param {Record<string, any>} rawRecord\n     */\n    normalizeRecord(rawRecord) {\n        const { fields, fieldMapping, isTimeHidden } = this.meta;\n\n        const startType = fields[fieldMapping.date_start].type;\n        const isAllDay =\n            startType === \"date\" ||\n            (fieldMapping.all_day && rawRecord[fieldMapping.all_day]) ||\n            false;\n        let start = isAllDay\n            ? deserializeDate(rawRecord[fieldMapping.date_start])\n            : deserializeDateTime(rawRecord[fieldMapping.date_start]);\n\n        let end = start;\n        let endType = startType;\n        if (fieldMapping.date_stop) {\n            endType = fields[fieldMapping.date_stop].type;\n            end = isAllDay\n                ? deserializeDate(rawRecord[fieldMapping.date_stop])\n                : deserializeDateTime(rawRecord[fieldMapping.date_stop]);\n        }\n\n        const duration = rawRecord[fieldMapping.date_delay] || 1;\n\n        if (isAllDay) {\n            start = start.startOf(\"day\");\n            end = end.startOf(\"day\");\n        }\n        if (!fieldMapping.date_stop && duration) {\n            end = start.plus({ hours: duration });\n        }\n\n        const showTime =\n            !(fieldMapping.all_day && rawRecord[fieldMapping.all_day]) &&\n            startType !== \"date\" &&\n            start.day === end.day;\n\n        const colorValue = rawRecord[fieldMapping.color];\n        const colorIndex = Array.isArray(colorValue) ? colorValue[0] : colorValue;\n\n        const title = rawRecord[fieldMapping.create_name_field || \"display_name\"];\n\n        return {\n            id: rawRecord.id,\n            title,\n            isAllDay,\n            start,\n            startType,\n            end,\n            endType,\n            duration,\n            colorIndex,\n            isHatched: rawRecord[\"is_hatched\"] || false,\n            isStriked: rawRecord[\"is_striked\"] || false,\n            isTimeHidden: isTimeHidden || !showTime,\n            isMonth: this.meta.scale === \"month\",\n            isSmall: this.env.isSmall,\n            rawRecord,\n        };\n    }\n\n    /**\n     * @protected\n     */\n    addFilterFields(record, filterInfo) {\n        return {\n            colorIndex: record.colorIndex,\n        };\n    }\n    //--------------------------------------------------------------------------\n\n    /**\n     * @protected\n     */\n    fetchFilters(resModel, fieldNames) {\n        return this.orm.searchRead(resModel, [[\"user_id\", \"=\", user.userId]], fieldNames);\n    }\n\n    getLocalStorageScale() {\n        const localScaleId = browser.localStorage.getItem(this.storageKey);\n        return this.meta.scales.includes(localScaleId) ? localScaleId : this.meta.scale;\n    }\n\n    /**\n     * @protected\n     */\n    async loadFilters(data) {\n        const previousSections = data.filterSections;\n        const sections = {};\n        const dynamicFiltersInfo = {};\n        for (const [fieldName, filterInfo] of Object.entries(this.meta.filtersInfo)) {\n            const previousSection = previousSections[fieldName];\n            if (filterInfo.writeResModel) {\n                sections[fieldName] = await this.loadFilterSection(\n                    fieldName,\n                    filterInfo,\n                    previousSection\n                );\n            } else {\n                dynamicFiltersInfo[fieldName] = { filterInfo, previousSection };\n            }\n        }\n        return { sections, dynamicFiltersInfo };\n    }\n    /**\n     * @protected\n     */\n    async loadFilterSection(fieldName, filterInfo, previousSection) {\n        const { filterFieldName, writeFieldName, writeResModel } = filterInfo;\n        const fields = [writeFieldName, filterFieldName].filter(Boolean);\n        const rawFilters = await this.fetchFilters(writeResModel, fields);\n        const previousFilters = previousSection ? previousSection.filters : [];\n\n        const filters = rawFilters.map((rawFilter) => {\n            const previousRecordFilter = previousFilters.find(\n                (f) => f.type === \"record\" && f.recordId === rawFilter.id\n            );\n            return this.makeFilterRecord(filterInfo, previousRecordFilter, rawFilter);\n        });\n\n        const field = this.meta.fields[fieldName];\n        const isUserOrPartner = [\"res.users\", \"res.partner\"].includes(field.relation);\n        if (isUserOrPartner) {\n            const previousUserFilter = previousFilters.find((f) => f.type === \"user\");\n            filters.push(\n                this.makeFilterUser(filterInfo, previousUserFilter, fieldName, rawFilters)\n            );\n        }\n\n        const previousAllFilter = previousFilters.find((f) => f.type === \"all\");\n        filters.push(this.makeFilterAll(previousAllFilter, isUserOrPartner, filterInfo.label));\n\n        return {\n            label: filterInfo.label,\n            fieldName,\n            filters,\n            avatar: {\n                field: filterInfo.avatarFieldName,\n                model: filterInfo.resModel,\n            },\n            hasAvatar: !!filterInfo.avatarFieldName,\n            write: {\n                field: writeFieldName,\n                model: writeResModel,\n            },\n            canCollapse: filters.length > 2,\n            canAddFilter: !!filterInfo.writeResModel,\n        };\n    }\n    /**\n     * @protected\n     */\n    async loadDynamicFilters(data, filtersInfo) {\n        const sections = {};\n        for (const [fieldName, { filterInfo, previousSection }] of Object.entries(filtersInfo)) {\n            sections[fieldName] = await this.loadDynamicFilterSection(\n                data,\n                fieldName,\n                filterInfo,\n                previousSection\n            );\n        }\n        return sections;\n    }\n    /**\n     * @protected\n     */\n    async loadDynamicFilterSection(data, fieldName, filterInfo, previousSection) {\n        const { fields, fieldMapping } = this.meta;\n        const field = fields[fieldName];\n        const previousFilters = previousSection ? previousSection.filters : [];\n\n        const rawFilters = Object.values(data.records).reduce((filters, record) => {\n            const rawValues = [\"many2many\", \"one2many\"].includes(field.type)\n                ? record.rawRecord[fieldName]\n                : [record.rawRecord[fieldName]];\n\n            for (const rawValue of rawValues) {\n                const value = Array.isArray(rawValue) ? rawValue[0] : rawValue;\n                if (!filters.find((f) => f.id === value)) {\n                    filters.push({\n                        id: value,\n                        [fieldName]: rawValue,\n                        ...this.addFilterFields(record, filterInfo),\n                    });\n                }\n            }\n            return filters;\n        }, []);\n\n        const { colorFieldName } = filterInfo;\n        const shouldFetchColor =\n            colorFieldName &&\n            (!fieldMapping.color ||\n                `${fieldName}.${colorFieldName}` !== fields[fieldMapping.color].related);\n        let rawColors = [];\n        if (shouldFetchColor) {\n            const relatedIds = rawFilters.map(({ id }) => id);\n            if (relatedIds.length) {\n                rawColors = await this.orm.searchRead(\n                    field.relation,\n                    [[\"id\", \"in\", relatedIds]],\n                    [colorFieldName]\n                );\n            }\n        }\n\n        const filters = rawFilters.map((rawFilter) => {\n            const previousDynamicFilter = previousFilters.find(\n                (f) => f.type === \"dynamic\" && f.value === rawFilter.id\n            );\n            return this.makeFilterDynamic(\n                filterInfo,\n                previousDynamicFilter,\n                fieldName,\n                rawFilter,\n                rawColors\n            );\n        });\n\n        return {\n            label: filterInfo.label,\n            fieldName,\n            filters,\n            avatar: {\n                field: filterInfo.avatarFieldName,\n                model: filterInfo.resModel,\n            },\n            hasAvatar: !!filterInfo.avatarFieldName,\n            write: {\n                field: filterInfo.writeFieldName,\n                model: filterInfo.writeResModel,\n            },\n            canCollapse: filters.length > 2,\n            canAddFilter: !!filterInfo.writeResModel,\n        };\n    }\n    /**\n     * @protected\n     */\n    makeFilterDynamic(filterInfo, previousFilter, fieldName, rawFilter, rawColors) {\n        const { fieldMapping, fields } = this.meta;\n        const rawValue = rawFilter[fieldName];\n        const value = Array.isArray(rawValue) ? rawValue[0] : rawValue;\n        const field = fields[fieldName];\n        const formatter = registry.category(\"formatters\").get(field.type);\n\n        const { colorFieldName } = filterInfo;\n        const colorField = fields[fieldMapping.color];\n        const hasFilterColorAttr = !!colorFieldName;\n        const sameRelatedModel =\n            colorField &&\n            (colorField.relation === field.relation ||\n                (colorField.related && colorField.related.startsWith(`${fieldName}.`)));\n        let colorIndex = null;\n        if (hasFilterColorAttr || sameRelatedModel) {\n            colorIndex = rawFilter.colorIndex;\n        }\n        if (rawColors.length) {\n            const rawColor = rawColors.find(({ id }) => id === value);\n            colorIndex = rawColor ? rawColor[colorFieldName] : 0;\n        }\n\n        return {\n            type: \"dynamic\",\n            recordId: null,\n            value,\n            label: formatter(rawValue, { field }) || this.defaultFilterLabel,\n            active: previousFilter ? previousFilter.active : true,\n            canRemove: false,\n            colorIndex,\n            hasAvatar: !!value,\n        };\n    }\n    /**\n     * @protected\n     */\n    makeFilterRecord(filterInfo, previousFilter, rawRecord) {\n        const { colorFieldName, filterFieldName, writeFieldName } = filterInfo;\n        const { fields, fieldMapping } = this.meta;\n        const raw = rawRecord[writeFieldName];\n        const value = Array.isArray(raw) ? raw[0] : raw;\n        const field = fields[writeFieldName];\n        const isX2Many = [\"many2many\", \"one2many\"].includes(field.type);\n        const formatter = registry.category(\"formatters\").get(isX2Many ? \"many2one\" : field.type);\n\n        const colorField = fields[fieldMapping.color];\n        const colorValue =\n            colorField &&\n            (() => {\n                const sameRelatedModel = colorField.relation === field.relation;\n                const sameRelatedField =\n                    colorField.related === `${writeFieldName}.${colorFieldName}`;\n                const shouldHaveColor = sameRelatedModel || sameRelatedField;\n                const colorToUse = raw ? value : rawRecord[fieldMapping.color];\n                return shouldHaveColor ? colorToUse : null;\n            })();\n        const colorIndex = Array.isArray(colorValue) ? colorValue[0] : colorValue;\n\n        let active = false;\n        if (previousFilter) {\n            active = previousFilter.active;\n        } else if (filterFieldName) {\n            active = rawRecord[filterFieldName];\n        }\n        return {\n            type: \"record\",\n            recordId: rawRecord.id,\n            value,\n            label: formatter(raw),\n            active,\n            canRemove: true,\n            colorIndex,\n            hasAvatar: !!value,\n        };\n    }\n    /**\n     * @protected\n     */\n    makeFilterUser(filterInfo, previousFilter, fieldName, rawRecords) {\n        const field = this.meta.fields[fieldName];\n        const userFieldName = field.relation === \"res.partner\" ? \"partnerId\" : \"userId\";\n        const value = user[userFieldName];\n\n        let colorIndex = value;\n        const rawRecord = rawRecords.find((r) => r[filterInfo.writeFieldName][0] === value);\n        if (filterInfo.colorFieldName && rawRecord) {\n            const colorValue = rawRecord[filterInfo.colorFieldName];\n            colorIndex = Array.isArray(colorValue) ? colorValue[0] : colorValue;\n        }\n\n        return {\n            type: \"user\",\n            recordId: null,\n            value,\n            label: user.name,\n            active: previousFilter ? previousFilter.active : true,\n            canRemove: false,\n            colorIndex,\n            hasAvatar: !!value,\n        };\n    }\n    /**\n     * @protected\n     */\n    makeFilterAll(previousAllFilter, isUserOrPartner, sectionLabel) {\n        return {\n            type: \"all\",\n            recordId: null,\n            value: \"all\",\n            label: isUserOrPartner ? _t(\"Everybody's calendars\") : _t(\"Everything\"),\n            active: previousAllFilter\n                ? previousAllFilter.active\n                : this.meta.allFilter[sectionLabel] || false,\n            canRemove: false,\n            colorIndex: null,\n            hasAvatar: false,\n        };\n    }\n}\n", "import { ActionSwiper } from \"@web/core/action_swiper/action_swiper\";\nimport { CalendarCommonRenderer } from \"./calendar_common/calendar_common_renderer\";\nimport { CalendarYearRenderer } from \"./calendar_year/calendar_year_renderer\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class CalendarRenderer extends Component {\n    static template = \"web.CalendarRenderer\";\n    static components = {\n        day: CalendarCommonRenderer,\n        week: CalendarCommonRenderer,\n        month: CalendarCommonRenderer,\n        year: CalendarYearRenderer,\n        ActionSwiper,\n    };\n    static props = {\n        model: Object,\n        isWeekendVisible: Boolean,\n        createRecord: Function,\n        editRecord: Function,\n        deleteRecord: Function,\n        setDate: Function,\n    };\n    get calendarComponent() {\n        return this.constructor.components[this.props.model.scale];\n    }\n    get calendarKey() {\n        return `${this.props.model.scale}_${this.props.model.date.valueOf()}`;\n    }\n    get actionSwiperProps() {\n        return {\n            onLeftSwipe: this.env.isSmall\n                ? { action: () => this.props.setDate(\"next\") }\n                : undefined,\n            onRightSwipe: this.env.isSmall\n                ? { action: () => this.props.setDate(\"previous\") }\n                : undefined,\n            animationOnMove: false,\n            animationType: \"forwards\",\n            swipeDistanceRatio: 6,\n            swipeInvalid: () => Boolean(document.querySelector(\".o_event.fc-mirror\")),\n        };\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { CalendarRenderer } from \"./calendar_renderer\";\nimport { CalendarArchParser } from \"./calendar_arch_parser\";\nimport { CalendarModel } from \"./calendar_model\";\nimport { CalendarController } from \"./calendar_controller\";\n\nexport const calendarView = {\n    type: \"calendar\",\n\n    searchMenuTypes: [\"filter\", \"favorite\"],\n\n    ArchParser: CalendarArchParser,\n    Controller: CalendarController,\n    Model: CalendarModel,\n    Renderer: CalendarRenderer,\n\n    buttonTemplate: \"web.CalendarController.controlButtons\",\n\n    props: (props, view) => {\n        const { ArchParser } = view;\n        const { arch, relatedModels, resModel } = props;\n        const archInfo = new ArchParser().parse(arch, relatedModels, resModel);\n        return {\n            ...props,\n            Model: view.Model,\n            Renderer: view.Renderer,\n            buttonTemplate: view.buttonTemplate,\n            archInfo,\n        };\n    },\n};\n\nregistry.category(\"views\").add(\"calendar\", calendarView);\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { formatDate } from \"@web/core/l10n/dates\";\nimport { getColor } from \"../colors\";\nimport { getFormattedDateSpan } from \"@web/views/calendar/utils\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class CalendarYearPopover extends Component {\n    static components = { Dialog };\n    static template = \"web.CalendarYearPopover\";\n    static subTemplates = {\n        popover: \"web.CalendarYearPopover.popover\",\n        body: \"web.CalendarYearPopover.body\",\n        footer: \"web.CalendarYearPopover.footer\",\n        record: \"web.CalendarYearPopover.record\",\n    };\n    static props = {\n        close: Function,\n        date: true,\n        model: Object,\n        records: Array,\n        createRecord: Function,\n        deleteRecord: Function,\n        editRecord: Function,\n    };\n\n    get recordGroups() {\n        return this.computeRecordGroups();\n    }\n\n    get dialogTitle() {\n        return formatDate(this.props.date, { format: \"DDD\" });\n    }\n\n    computeRecordGroups() {\n        const recordGroups = this.groupRecords();\n        return this.getSortedRecordGroups(recordGroups);\n    }\n    groupRecords() {\n        const recordGroups = {};\n        for (const record of this.props.records) {\n            const start = record.start;\n            const end = record.end;\n\n            const duration = end.diff(start, \"days\").days;\n            const modifiedRecord = Object.create(record);\n            modifiedRecord.startHour =\n                !record.isAllDay && duration < 1 ? start.toFormat(\"HH:mm\") : \"\";\n\n            const formattedDate = getFormattedDateSpan(start, end);\n            if (!(formattedDate in recordGroups)) {\n                recordGroups[formattedDate] = {\n                    title: formattedDate,\n                    start,\n                    end,\n                    records: [],\n                };\n            }\n            recordGroups[formattedDate].records.push(modifiedRecord);\n        }\n        return Object.values(recordGroups);\n    }\n    getRecordClass(record) {\n        const { colorIndex } = record;\n        const color = getColor(colorIndex);\n        if (color && typeof color === \"number\") {\n            return `o_calendar_color_${color}`;\n        }\n        return \"\";\n    }\n    getRecordStyle(record) {\n        const { colorIndex } = record;\n        const color = getColor(colorIndex);\n        if (color && typeof color === \"string\") {\n            return `background-color: ${color};`;\n        }\n        return \"\";\n    }\n    getSortedRecordGroups(recordGroups) {\n        return recordGroups.sort((a, b) => {\n            if (a.start.hasSame(a.end, \"days\")) {\n                return Number.MIN_SAFE_INTEGER;\n            } else if (b.start.hasSame(b.end, \"days\")) {\n                return Number.MAX_SAFE_INTEGER;\n            } else if (a.start.toMillis() - b.start.toMillis() === 0) {\n                return a.end.toMillis() - b.end.toMillis();\n            }\n            return a.start.toMillis() - b.start.toMillis();\n        });\n    }\n\n    onCreateButtonClick() {\n        this.props.createRecord({\n            start: this.props.date,\n            isAllDay: true,\n        });\n        this.props.close();\n    }\n    onRecordClick(record) {\n        this.props.editRecord(record);\n        this.props.close();\n    }\n}\n", "import { localization } from \"@web/core/l10n/localization\";\nimport { useDebounced } from \"@web/core/utils/timing\";\nimport { getColor } from \"../colors\";\nimport { useCalendarPopover, useFullCalendar } from \"../hooks\";\nimport { CalendarYearPopover } from \"./calendar_year_popover\";\nimport { makeWeekColumn } from \"@web/views/calendar/calendar_common/calendar_common_week_column\";\nimport { getLocalWeekNumber } from \"@web/core/l10n/dates\";\n\nimport { Component, useEffect, useRef } from \"@odoo/owl\";\n\nexport class CalendarYearRenderer extends Component {\n    static components = {\n        Popover: CalendarYearPopover,\n    };\n    static template = \"web.CalendarYearRenderer\";\n    static props = {\n        model: Object,\n        displayName: { type: String, optional: true },\n        isWeekendVisible: { type: Boolean, optional: true },\n        createRecord: Function,\n        editRecord: Function,\n        deleteRecord: Function,\n        setDate: { type: Function, optional: true },\n    };\n\n    setup() {\n        this.months = luxon.Info.months();\n        this.fcs = {};\n        for (const month of this.months) {\n            this.fcs[month] = useFullCalendar(\n                `fullCalendar-${month}`,\n                this.getOptionsForMonth(month)\n            );\n        }\n        this.popover = useCalendarPopover(this.constructor.components.Popover);\n        this.rootRef = useRef(\"root\");\n        this.onWindowResizeDebounced = useDebounced(this.onWindowResize, 200);\n\n        useEffect(() => {\n            this.updateSize();\n        });\n    }\n\n    get options() {\n        return {\n            dayHeaderFormat: \"EEEEE\",\n            dateClick: this.onDateClick,\n            dayCellClassNames: this.getDayCellClassNames,\n            initialDate: this.props.model.date.toISO(),\n            initialView: \"dayGridMonth\",\n            direction: localization.direction,\n            droppable: true,\n            editable: this.props.model.canEdit,\n            dayMaxEventRows: this.props.model.eventLimit,\n            eventClassNames: this.eventClassNames,\n            eventDidMount: this.onEventDidMount,\n            eventResizableFromStart: true,\n            events: (_, successCb) => successCb(this.mapRecordsToEvents()),\n            firstDay: this.props.model.firstDayOfWeek,\n            headerToolbar: { start: false, center: \"title\", end: false },\n            height: \"auto\",\n            locale: luxon.Settings.defaultLocale,\n            longPressDelay: 500,\n            navLinks: false,\n            nowIndicator: true,\n            select: this.onSelect,\n            selectMinDistance: 5, // needed to not trigger select when click\n            selectMirror: true,\n            selectable: this.props.model.canCreate,\n            showNonCurrentDates: false,\n            timeZone: luxon.Settings.defaultZone.name,\n            titleFormat: { month: \"long\", year: \"numeric\" },\n            unselectAuto: false,\n            weekNumberCalculation: (date) => getLocalWeekNumber(date),\n            weekNumbers: false,\n            weekNumberFormat: { week: \"numeric\" },\n            windowResize: this.onWindowResizeDebounced,\n            eventContent: this.onEventContent,\n            viewDidMount: this.viewDidMount,\n            weekends: this.props.isWeekendVisible,\n        };\n    }\n\n    get customOptions() {\n        return {\n            weekNumbersWithinDays: true,\n        };\n    }\n\n    viewDidMount({ el, view }) {\n        const showWeek = view.calendar.currentData.options.weekNumbers;\n        const weekText = view.calendar.currentData.options.weekText;\n        const weekColumn = !this.customOptions.weekNumbersWithinDays;\n        if (showWeek && weekColumn) {\n            makeWeekColumn({ el, weekText });\n        }\n    }\n\n    mapRecordsToEvents() {\n        return Object.values(this.props.model.records).map((r) => this.convertRecordToEvent(r));\n    }\n    convertRecordToEvent(record) {\n        return {\n            id: record.id,\n            title: record.title,\n            start: record.start.toISO(),\n            end: record.end.plus({ day: 1 }).toISO(),\n            allDay: true,\n            display: \"background\",\n        };\n    }\n    getDateWithMonth(month) {\n        return this.props.model.date.set({ month: this.months.indexOf(month) + 1 }).toISO();\n    }\n    getOptionsForMonth(month) {\n        return {\n            ...this.options,\n            initialDate: this.getDateWithMonth(month),\n        };\n    }\n    getPopoverProps(date, records) {\n        return {\n            date,\n            records,\n            model: this.props.model,\n            createRecord: this.props.createRecord,\n            deleteRecord: this.props.deleteRecord,\n            editRecord: this.props.editRecord,\n        };\n    }\n    openPopover(target, date, records) {\n        this.popover.open(target, this.getPopoverProps(date, records), \"o_cw_popover\");\n    }\n    unselect() {\n        for (const fc of Object.values(this.fcs)) {\n            fc.api.unselect();\n        }\n    }\n    updateSize() {\n        const height = window.innerHeight - this.rootRef.el.getBoundingClientRect().top;\n        this.rootRef.el.style.height = `${height}px`;\n    }\n\n    onDateClick(info) {\n        if (this.env.isSmall) {\n            this.props.model.load({\n                date: luxon.DateTime.fromISO(info.dateStr),\n                scale: \"day\",\n            });\n            return;\n        }\n\n        // With date value we don't want to change the time, we need the exact date\n        const date = luxon.DateTime.fromISO(info.dateStr);\n        const records = Object.values(this.props.model.records).filter((r) =>\n            luxon.Interval.fromDateTimes(r.start.startOf(\"day\"), r.end.endOf(\"day\")).contains(date)\n        );\n\n        this.popover.close();\n        if (records.length) {\n            const target = info.dayEl;\n            this.openPopover(target, date, records);\n        } else if (this.props.model.canCreate) {\n            this.props.createRecord({\n                // With date value we don't want to change the time, we need the exact date\n                start: luxon.DateTime.fromISO(info.dateStr),\n                isAllDay: true,\n            });\n        }\n    }\n    getDayCellClassNames(info) {\n        const date = luxon.DateTime.fromJSDate(info.date).toISODate();\n        if (this.props.model.unusualDays.includes(date)) {\n            return [\"o_calendar_disabled\"];\n        }\n        return [];\n    }\n    eventClassNames({ event }) {\n        const classesToAdd = [];\n        classesToAdd.push(\"o_event\");\n        const record = this.props.model.records[event.id];\n        if (record) {\n            const color = getColor(record.colorIndex);\n            if (typeof color === \"number\") {\n                classesToAdd.push(`o_calendar_color_${color}`);\n            } else if (typeof color !== \"string\") {\n                classesToAdd.push(\"o_calendar_color_0\");\n            }\n\n            if (record.isHatched) {\n                classesToAdd.push(\"o_event_hatched\");\n            }\n            if (record.isStriked) {\n                classesToAdd.push(\"o_event_striked\");\n            }\n        }\n        return classesToAdd;\n    }\n    onEventDidMount(info) {\n        const { el, event } = info;\n        el.dataset.eventId = event.id;\n        const record = this.props.model.records[event.id];\n        if (record) {\n            const color = getColor(record.colorIndex);\n            if (typeof color === \"string\") {\n                el.style.backgroundColor = color;\n            }\n        }\n    }\n    async onSelect(info) {\n        this.popover.close();\n        await this.props.createRecord({\n            // With date value we don't want to change the time, we need the exact date\n            start: luxon.DateTime.fromISO(info.startStr),\n            end: luxon.DateTime.fromISO(info.endStr).minus({ days: 1 }),\n            isAllDay: true,\n        });\n        this.unselect();\n    }\n    onWindowResize() {\n        this.updateSize();\n    }\n\n    onEventContent(info) {\n        // Remove the title on the background event like in FCv4\n        if (info.event.display?.includes(\"background\")) {\n            return null;\n        }\n    }\n}\n", "const CSS_COLOR_REGEX = /^((#[A-F0-9]{3})|(#[A-F0-9]{6})|((hsl|rgb)a?\\(\\s*(?:(\\s*\\d{1,3}%?\\s*),?){3}(\\s*,[0-9.]{1,4})?\\))|)$/i;\nconst colorMap = new Map();\n\nexport function getColor(key) {\n    if (!key) {\n        return false;\n    }\n    if (colorMap.has(key)) {\n        return colorMap.get(key);\n    }\n\n    // check if the key is a css color\n    if (typeof key === \"string\" && key.match(CSS_COLOR_REGEX)) {\n        colorMap.set(key, key);\n    } else if (typeof key === \"number\") {\n        colorMap.set(key, ((key - 1) % 55) + 1);\n    } else {\n        colorMap.set(key, (((colorMap.size + 1) * 5) % 24) + 1);\n    }\n\n    return colorMap.get(key);\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { AutoComplete } from \"@web/core/autocomplete/autocomplete\";\nimport { Transition } from \"@web/core/transition\";\nimport { useOwnedDialogs, useService } from \"@web/core/utils/hooks\";\nimport { SelectCreateDialog } from \"@web/views/view_dialogs/select_create_dialog\";\nimport { getColor } from \"../colors\";\nimport { Component, useState } from \"@odoo/owl\";\n\nlet nextId = 1;\n\nexport class CalendarFilterPanel extends Component {\n    static components = {\n        AutoComplete,\n        Transition,\n    };\n    static template = \"web.CalendarFilterPanel\";\n    static subTemplates = {\n        filter: \"web.CalendarFilterPanel.filter\",\n    };\n    static props = {\n        model: Object,\n    };\n\n    setup() {\n        this.state = useState({\n            collapsed: {},\n            fieldRev: 1,\n        });\n        this.addDialog = useOwnedDialogs();\n        this.orm = useService(\"orm\");\n    }\n\n    getFilterColor(filter) {\n        return filter.colorIndex !== null ? \"o_cw_filter_color_\" + getColor(filter.colorIndex) : \"\";\n    }\n\n    getAutoCompleteProps(section) {\n        return {\n            autoSelect: true,\n            resetOnSelect: true,\n            placeholder: _t(\"+ Add %s\", section.label),\n            sources: [\n                {\n                    placeholder: _t(\"Loading...\"),\n                    options: (request) => this.loadSource(section, request),\n                    optionTemplate: \"web.CalendarFilterPanel.autocomplete.options\",\n                },\n            ],\n            onSelect: (option, params = {}) => {\n                if (option.action) {\n                    option.action(params);\n                    return;\n                }\n                this.props.model.createFilter(section.fieldName, option.value);\n            },\n            value: \"\",\n        };\n    }\n\n    async loadSource(section, request) {\n        const resModel = this.props.model.fields[section.fieldName].relation;\n        const domain = [\n            [\"id\", \"not in\", section.filters.filter((f) => f.type !== \"all\").map((f) => f.value)],\n        ];\n        const records = await this.orm.call(resModel, \"name_search\", [], {\n            name: request,\n            operator: \"ilike\",\n            args: domain,\n            limit: 8,\n            context: {},\n        });\n\n        const options = records.map((result) => ({\n            value: result[0],\n            label: result[1],\n            model: resModel,\n        }));\n\n        if (records.length > 7) {\n            options.push({\n                label: _t(\"Search More...\"),\n                action: () => this.onSearchMore(section, resModel, domain, request),\n                classList: \"o_calendar_dropdown_option\",\n            });\n        }\n\n        if (records.length === 0) {\n            options.push({\n                label: _t(\"No records\"),\n                classList: \"o_m2o_no_result\",\n                unselectable: true,\n            });\n        }\n\n        return options;\n    }\n\n    async onSearchMore(section, resModel, domain, request) {\n        const dynamicFilters = [];\n        if (request.length) {\n            const nameGets = await this.orm.call(resModel, \"name_search\", [], {\n                name: request,\n                args: domain,\n                operator: \"ilike\",\n                context: {},\n            });\n            dynamicFilters.push({\n                description: _t(\"Quick search: %s\", request),\n                domain: [[\"id\", \"in\", nameGets.map((nameGet) => nameGet[0])]],\n            });\n        }\n        const title = _t(\"Search: %s\", section.label);\n        const dialogProps = {\n            title,\n            noCreate: true,\n            multiSelect: true,\n            resModel,\n            context: {},\n            domain,\n            onSelected: (resId) => this.props.model.createFilter(section.fieldName, resId),\n            dynamicFilters,\n        };\n\n        const updatedProps = this.updateSelectCreateDialogProps(dialogProps);\n        this.addDialog(SelectCreateDialog, updatedProps);\n    }\n\n    updateSelectCreateDialogProps(props) {\n        return props;\n    }\n\n    get nextFilterId() {\n        nextId += 1;\n        return nextId;\n    }\n\n    isAllActive(section) {\n        let active = true;\n        for (const filter of section.filters) {\n            if (filter.type !== \"all\" && !filter.active) {\n                active = false;\n                break;\n            }\n        }\n        return active;\n    }\n    getFilterTypePriority(type) {\n        return [\"user\", \"record\", \"dynamic\", \"all\"].indexOf(type);\n    }\n    getSortedFilters(section) {\n        return section.filters.slice().sort((a, b) => {\n            if (a.type === b.type) {\n                const va = a.value ? -1 : 0;\n                const vb = b.value ? -1 : 0;\n                //Condition to put unvaluable item (eg: Open Shifts) at the end of the sorted list.\n                if (a.type === \"dynamic\" && va !== vb) {\n                    return va - vb;\n                }\n                return a.label.localeCompare(b.label, undefined, {\n                    numeric: true,\n                    sensitivity: \"base\",\n                    ignorePunctuation: true,\n                });\n            } else {\n                return this.getFilterTypePriority(a.type) - this.getFilterTypePriority(b.type);\n            }\n        });\n    }\n\n    toggleSection(section) {\n        if (section.canCollapse) {\n            this.state.collapsed[section.fieldName] = !this.state.collapsed[section.fieldName];\n        }\n    }\n\n    isSectionCollapsed(section) {\n        return this.state.collapsed[section.fieldName] || false;\n    }\n\n    onFilterInputChange(section, filter, ev) {\n        this.props.model.updateFilters(section.fieldName, {\n            [filter.value]: ev.target.checked,\n        });\n    }\n\n    onAllFilterInputChange(section, ev) {\n        const filters = {};\n        for (const filter of section.filters) {\n            if (filter.type !== \"all\") {\n                filters[filter.value] = ev.target.checked;\n            }\n        }\n        this.props.model.updateFilters(section.fieldName, filters);\n    }\n\n    onFilterRemoveBtnClick(section, filter) {\n        this.props.model.unlinkFilter(section.fieldName, filter.recordId);\n    }\n\n    onFieldChanged(fieldName, filterValue) {\n        this.state.fieldRev += 1;\n        this.props.model.createFilter(fieldName, filterValue);\n    }\n}\n", "import { loadBundle } from \"@web/core/assets\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport {\n    onMounted,\n    onPatched,\n    onWillStart,\n    onWillUnmount,\n    useComponent,\n    useExternalListener,\n    useRef,\n} from \"@odoo/owl\";\n\nexport function useCalendarPopover(component) {\n    const owner = useComponent();\n    let popoverClass = \"\";\n    const popoverOptions = { position: \"right\", onClose: cleanup };\n    Object.defineProperty(popoverOptions, \"popoverClass\", { get: () => popoverClass });\n    const popover = usePopover(component, popoverOptions);\n    const dialog = useService(\"dialog\");\n    let removeDialog = null;\n    let fcPopover;\n    useExternalListener(\n        window,\n        \"mousedown\",\n        (ev) => {\n            if (fcPopover) {\n                // do not let fullcalendar popover close when our own popover is open\n                ev.stopPropagation();\n            }\n        },\n        { capture: true }\n    );\n    function cleanup() {\n        fcPopover = null;\n        removeDialog = null;\n    }\n    function close() {\n        removeDialog?.();\n        popover.close();\n        cleanup();\n    }\n    return {\n        close,\n        open(target, props, popoverClassToUse) {\n            fcPopover = target.closest(\".fc-popover\");\n            if (owner.env.isSmall) {\n                close();\n                removeDialog = dialog.add(component, props, { onClose: cleanup });\n            } else {\n                popoverClass = popoverClassToUse;\n                popover.open(target, props);\n            }\n        },\n    };\n}\n\nexport function useClickHandler(singleClickCb, doubleClickCb) {\n    const component = useComponent();\n    let clickTimeoutId = null;\n    return function handle(...args) {\n        if (clickTimeoutId) {\n            doubleClickCb.call(component, ...args);\n            browser.clearTimeout(clickTimeoutId);\n            clickTimeoutId = null;\n        } else {\n            clickTimeoutId = browser.setTimeout(() => {\n                singleClickCb.call(component, ...args);\n                clickTimeoutId = null;\n            }, 250);\n        }\n    };\n}\n\nexport function useFullCalendar(refName, params) {\n    const component = useComponent();\n    const ref = useRef(refName);\n    let instance = null;\n\n    function boundParams() {\n        const newParams = {};\n        for (const key in params) {\n            const value = params[key];\n            newParams[key] = typeof value === \"function\" ? value.bind(component) : value;\n        }\n        return newParams;\n    }\n\n    onWillStart(async () => await loadBundle(\"web.fullcalendar_lib\"));\n\n    onMounted(() => {\n        try {\n            instance = new FullCalendar.Calendar(ref.el, boundParams());\n            instance.render();\n        } catch (e) {\n            throw new Error(`Cannot instantiate FullCalendar\\n${e.message}`);\n        }\n    });\n\n    onPatched(() => {\n        instance.refetchEvents();\n        instance.setOption(\"weekends\", component.props.isWeekendVisible);\n        if (params.weekNumbers && component.props.model.scale === \"year\") {\n            instance.destroy();\n            instance.render();\n        }\n    });\n    onWillUnmount(() => {\n        instance.destroy();\n    });\n\n    return {\n        get api() {\n            return instance;\n        },\n        get el() {\n            return ref.el;\n        },\n    };\n}\n", "import { Component } from \"@odoo/owl\";\nimport { getColor } from \"../colors\";\n\nexport class CalendarMobileFilterPanel extends Component {\n    static components = {};\n    static template = \"web.CalendarMobileFilterPanel\";\n    static props = {\n        model: Object,\n        sideBarShown: Boolean,\n        toggleSideBar: Function,\n    };\n    get caretDirection() {\n        return this.props.sideBarShown ? \"down\" : \"left\";\n    }\n    getFilterColor(filter) {\n        return `o_color_${getColor(filter.colorIndex)}`;\n    }\n    getFilterTypePriority(type) {\n        return [\"user\", \"record\", \"dynamic\", \"all\"].indexOf(type);\n    }\n    getSortedFilters(section) {\n        return section.filters.slice().sort((a, b) => {\n            if (a.type === b.type) {\n                const va = a.value ? -1 : 0;\n                const vb = b.value ? -1 : 0;\n                //Condition to put unvaluable item (eg: Open Shifts) at the end of the sorted list.\n                if (a.type === \"dynamic\" && va !== vb) {\n                    return va - vb;\n                }\n                return a.label.localeCompare(b.label, undefined, {\n                    numeric: true,\n                    sensitivity: \"base\",\n                    ignorePunctuation: true,\n                });\n            } else {\n                return this.getFilterTypePriority(a.type) - this.getFilterTypePriority(b.type);\n            }\n        });\n    }\n}\n", "import { useAutofocus, useService } from \"@web/core/utils/hooks\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class CalendarQuickCreate extends Component {\n    static template = \"web.CalendarQuickCreate\";\n    static components = {\n        Dialog,\n    };\n    static props = {\n        title: { type: String, optional: true },\n        close: Function,\n        record: Object,\n        model: Object,\n        editRecord: Function,\n    };\n\n    setup() {\n        this.titleRef = useAutofocus({ refName: \"title\" });\n        this.notification = useService(\"notification\");\n        this.creatingRecord = false;\n    }\n\n    get dialogTitle() {\n        return _t(\"New Event\");\n    }\n\n    get recordTitle() {\n        return this.titleRef.el.value.trim();\n    }\n    get record() {\n        return {\n            ...this.props.record,\n            title: this.recordTitle,\n        };\n    }\n\n    editRecord() {\n        this.props.editRecord(this.record);\n        this.props.close();\n    }\n    async createRecord() {\n        if (this.creatingRecord) {\n            return;\n        }\n\n        if (this.recordTitle) {\n            try {\n                this.creatingRecord = true;\n                await this.props.model.createRecord(this.record);\n                this.props.close();\n            } catch {\n                this.editRecord();\n            }\n        } else {\n            this.titleRef.el.classList.add(\"o_field_invalid\");\n            this.notification.add(_t(\"Meeting Subject\"), {\n                title: _t(\"Invalid fields\"),\n                type: \"danger\",\n            });\n        }\n    }\n\n    onInputKeyup(ev) {\n        switch (ev.key) {\n            case \"Enter\":\n                this.createRecord();\n                break;\n            case \"Escape\":\n                this.props.close();\n                break;\n        }\n    }\n    onCreateBtnClick() {\n        this.createRecord();\n    }\n    onEditBtnClick() {\n        this.editRecord();\n    }\n    onCancelBtnClick() {\n        this.props.close();\n    }\n}\n", "export function getFormattedDateSpan(start, end) {\n    const isSameDay = start.hasSame(end, \"days\");\n\n    if (!isSameDay && start.hasSame(end, \"month\")) {\n        // Simplify date-range if an event occurs into the same month (eg. \"August 4-5, 2019\")\n        return start.toFormat(\"LLLL d\") + \"-\" + end.toFormat(\"d, y\");\n    } else {\n        return isSameDay\n            ? start.toFormat(\"DDD\")\n            : start.toFormat(\"DDD\") + \" - \" + end.toFormat(\"DDD\");\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { editModelDebug } from \"@web/core/debug/debug_utils\";\nimport { formatDateTime, deserializeDateTime } from \"@web/core/l10n/dates\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { formatMany2one } from \"@web/views/fields/formatters\";\nimport { FormViewDialog } from \"@web/views/view_dialogs/form_view_dialog\";\n\nimport { Component, onWillStart, useState, xml } from \"@odoo/owl\";\nimport { serializeDate, serializeDateTime } from \"../core/l10n/dates\";\n\nconst debugRegistry = registry.category(\"debug\");\n\n//------------------------------------------------------------------------------\n// Get view\n//------------------------------------------------------------------------------\n\nclass GetViewDialog extends Component {\n    static template = \"web.DebugMenu.GetViewDialog\";\n    static components = { Dialog };\n    static props = {\n        arch: { type: String },\n        close: { type: Function },\n    };\n}\n\nexport function getView({ component, env }) {\n    return {\n        type: \"item\",\n        description: _t(\"Computed Arch\"),\n        callback: () => {\n            env.services.dialog.add(GetViewDialog, { arch: component.env.config.rawArch });\n        },\n        sequence: 270,\n        section: \"ui\",\n    };\n}\n\ndebugRegistry.category(\"view\").add(\"getView\", getView);\n\n//------------------------------------------------------------------------------\n// Edit View\n//------------------------------------------------------------------------------\n\nexport function editView({ accessRights, component, env }) {\n    if (!accessRights.canEditView) {\n        return null;\n    }\n    const { viewId, viewType: type } = component.env.config;\n    if (!type) {\n        return;\n    }\n    const displayName = type[0].toUpperCase() + type.slice(1);\n    const description = _t(\"View: %(displayName)s\", { displayName });\n    return {\n        type: \"item\",\n        description,\n        callback: () => {\n            editModelDebug(env, description, \"ir.ui.view\", viewId);\n        },\n        sequence: 240,\n        section: \"ui\",\n    };\n}\n\ndebugRegistry.category(\"view\").add(\"editView\", editView);\n\n//------------------------------------------------------------------------------\n// Edit SearchView\n//------------------------------------------------------------------------------\n\nexport function editSearchView({ accessRights, component, env }) {\n    if (!accessRights.canEditView) {\n        return null;\n    }\n    const { searchViewId } = component.componentProps.info;\n    if (searchViewId === undefined) {\n        return null;\n    }\n    const description = _t(\"SearchView\");\n    return {\n        type: \"item\",\n        description,\n        callback: () => {\n            editModelDebug(env, description, \"ir.ui.view\", searchViewId);\n        },\n        sequence: 230,\n        section: \"ui\",\n    };\n}\n\ndebugRegistry.category(\"view\").add(\"editSearchView\", editSearchView);\n\n// -----------------------------------------------------------------------------\n// View Metadata\n// -----------------------------------------------------------------------------\n\nclass GetMetadataDialog extends Component {\n    static template = \"web.DebugMenu.GetMetadataDialog\";\n    static components = { Dialog };\n    static props = {\n        resModel: String,\n        resId: Number,\n        close: Function,\n    };\n    setup() {\n        this.orm = useService(\"orm\");\n        this.dialogService = useService(\"dialog\");\n        this.title = _t(\"View Metadata\");\n        this.state = useState({});\n        onWillStart(() => this.loadMetadata());\n    }\n\n    onClickCreateXmlid() {\n        const context = Object.assign({}, this.context, {\n            default_module: \"__custom__\",\n            default_res_id: this.state.id,\n            default_model: this.props.resModel,\n        });\n        this.dialogService.add(FormViewDialog, {\n            context,\n            onRecordSaved: () => this.loadMetadata(),\n            resModel: \"ir.model.data\",\n        });\n    }\n\n    async toggleNoupdate() {\n        await this.env.services.orm.call(\"ir.model.data\", \"toggle_noupdate\", [\n            this.props.resModel,\n            this.state.id,\n        ]);\n        await this.loadMetadata();\n    }\n\n    async loadMetadata() {\n        const args = [[this.props.resId]];\n        const result = await this.orm.call(this.props.resModel, \"get_metadata\", args);\n        const metadata = result[0];\n        this.state.id = metadata.id;\n        this.state.xmlid = metadata.xmlid;\n        this.state.xmlids = metadata.xmlids;\n        this.state.noupdate = metadata.noupdate;\n        this.state.creator = formatMany2one(metadata.create_uid);\n        this.state.lastModifiedBy = formatMany2one(metadata.write_uid);\n        this.state.createDate = formatDateTime(deserializeDateTime(metadata.create_date));\n        this.state.writeDate = formatDateTime(deserializeDateTime(metadata.write_date));\n    }\n}\n\nexport function viewMetadata({ component, env }) {\n    const resId = component.model.root.resId;\n    if (!resId) {\n        return null; // No record\n    }\n    return {\n        type: \"item\",\n        description: _t(\"Metadata\"),\n        callback: () => {\n            env.services.dialog.add(GetMetadataDialog, {\n                resModel: component.props.resModel,\n                resId,\n            });\n        },\n        sequence: 110,\n        section: \"record\",\n    };\n}\n\ndebugRegistry.category(\"form\").add(\"viewMetadata\", viewMetadata);\n\n// -----------------------------------------------------------------------------\n// View Raw Record Data\n// -----------------------------------------------------------------------------\n\nclass RawRecordDialog extends Component {\n    static template = xml`\n        <Dialog title=\"props.title\">\n            <pre t-esc=\"content\"/>\n        </Dialog>\n    `;\n    static components = { Dialog };\n    static props = {\n        record: { type: Object },\n        title: { type: String },\n        close: { type: Function },\n    };\n    get content() {\n        const record = this.props.record;\n        return JSON.stringify(record, Object.keys(record).sort(), 2);\n    }\n}\n\nexport function viewRawRecord({ component, env }) {\n    const { resId, resModel } = component.model.config;\n    if (!resId) {\n        return null;\n    }\n    const description = _t(\"Data\");\n    return {\n        type: \"item\",\n        description,\n        callback: async () => {\n            const records = await component.model.orm.read(resModel, [resId]);\n            env.services.dialog.add(RawRecordDialog, {\n                title: _t(\"Data: %(model)s(%(id)s)\", { model: resModel, id: resId }),\n                record: records[0],\n            });\n        },\n        sequence: 120,\n        section: \"record\",\n    };\n}\n\ndebugRegistry.category(\"form\").add(\"viewRawRecord\", viewRawRecord);\n\n// -----------------------------------------------------------------------------\n// Set Defaults\n// -----------------------------------------------------------------------------\n\nclass SetDefaultDialog extends Component {\n    static template = \"web.DebugMenu.SetDefaultDialog\";\n    static components = { Dialog };\n    static props = {\n        record: { type: Object },\n        fieldNodes: { type: Object },\n        close: { type: Function },\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.state = {\n            fieldToSet: \"\",\n            condition: \"\",\n            scope: \"self\",\n        };\n        this.fields = this.props.record.fields;\n        this.activeFields = this.props.record.activeFields;\n        this.fieldNamesInView = this.props.record.fieldNames;\n        this.fieldNamesBlackList = [\"message_attachment_count\"];\n        this.fieldsValues = this.props.record.data;\n        this.modifierDatas = {};\n        this.defaultFields = this.getDefaultFields();\n        this.conditions = this.getConditions();\n    }\n\n    getDefaultFields() {\n        return this.fieldNamesInView\n            .filter((fieldName) => !this.fieldNamesBlackList.includes(fieldName))\n            .map((fieldName) => {\n                const fieldInfo = this.fields[fieldName];\n                const valueDisplayed = this.display(fieldInfo, this.fieldsValues[fieldName]);\n                const value = valueDisplayed[0];\n                const displayed = valueDisplayed[1];\n                const evalContext = this.props.record.evalContextWithVirtualIds;\n                // ignore fields which are empty, invisible, readonly, o2m or m2m\n                if (\n                    !value ||\n                    evaluateBooleanExpr(this.activeFields[fieldName].invisible, evalContext) ||\n                    evaluateBooleanExpr(this.activeFields[fieldName].readonly, evalContext) ||\n                    fieldInfo.type === \"one2many\" ||\n                    fieldInfo.type === \"many2many\" ||\n                    fieldInfo.type === \"binary\" ||\n                    Object.entries(this.props.fieldNodes)\n                        .filter(([key, value]) => value.name === fieldName)\n                        .some(([key, value]) => value.options.isPassword)\n                ) {\n                    return false;\n                }\n                return {\n                    name: fieldName,\n                    string: fieldInfo.string,\n                    value,\n                    displayed,\n                };\n            })\n            .filter((val) => val)\n            .sort((field) => field.string);\n    }\n\n    getConditions() {\n        return this.fieldNamesInView\n            .filter((fieldName) => {\n                const fieldInfo = this.fields[fieldName];\n                return fieldInfo.change_default;\n            })\n            .map((fieldName) => {\n                const fieldInfo = this.fields[fieldName];\n                const valueDisplayed = this.display(fieldInfo, this.fieldsValues[fieldName]);\n                const value = valueDisplayed[0];\n                const displayed = valueDisplayed[1];\n                return {\n                    name: fieldName,\n                    string: fieldInfo.string,\n                    value: value,\n                    displayed: displayed,\n                };\n            });\n    }\n\n    display(fieldInfo, value) {\n        let displayed = value;\n        if (value && fieldInfo.type === \"many2one\") {\n            displayed = value[1];\n            value = value[0];\n        } else if (value && fieldInfo.type === \"selection\") {\n            displayed = fieldInfo.selection.find((option) => {\n                return option[0] === value;\n            })[1];\n        }\n        return [value, displayed];\n    }\n\n    async saveDefault() {\n        if (!this.state.fieldToSet) {\n            return;\n        }\n        let fieldToSet = this.defaultFields.find((field) => {\n            return field.name === this.state.fieldToSet;\n        }).value;\n\n        if (fieldToSet.constructor.name.toLowerCase() === \"date\") {\n            fieldToSet = serializeDate(fieldToSet);\n        } else if (fieldToSet.constructor.name.toLowerCase() === \"datetime\") {\n            fieldToSet = serializeDateTime(fieldToSet);\n        }\n        await this.orm.call(\"ir.default\", \"set\", [\n            this.props.record.resModel,\n            this.state.fieldToSet,\n            fieldToSet,\n            this.state.scope === \"self\",\n            true,\n            this.state.condition || false,\n        ]);\n        this.props.close();\n    }\n}\n\nexport function setDefaults({ component, env }) {\n    return {\n        type: \"item\",\n        description: _t(\"Set Default Values\"),\n        callback: () => {\n            env.services.dialog.add(SetDefaultDialog, {\n                record: component.model.root,\n                fieldNodes: component.props.archInfo.fieldNodes,\n            });\n        },\n        sequence: 150,\n        section: \"record\",\n    };\n}\ndebugRegistry.category(\"form\").add(\"setDefaults\", setDefaults);\n\n//------------------------------------------------------------------------------\n// Manage Attachments\n//------------------------------------------------------------------------------\n\nexport function manageAttachments({ component, env }) {\n    const resId = component.model.root.resId;\n    if (!resId) {\n        return null; // No record\n    }\n    const description = _t(\"Attachments\");\n    return {\n        type: \"item\",\n        description,\n        callback: () => {\n            env.services.action.doAction({\n                res_model: \"ir.attachment\",\n                name: description,\n                views: [\n                    [false, \"list\"],\n                    [false, \"form\"],\n                ],\n                type: \"ir.actions.act_window\",\n                domain: [\n                    [\"res_model\", \"=\", component.props.resModel],\n                    [\"res_id\", \"=\", resId],\n                ],\n                context: {\n                    default_res_model: component.props.resModel,\n                    default_res_id: resId,\n                    skip_res_field_check: true,\n                },\n            });\n        },\n        sequence: 140,\n        section: \"record\",\n    };\n}\n\ndebugRegistry.category(\"form\").add(\"manageAttachments\", manageAttachments);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useBus } from \"@web/core/utils/hooks\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { CodeEditor } from \"@web/core/code_editor/code_editor\";\nimport { Component, useState } from \"@odoo/owl\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\nimport { formatText } from \"@web/views/fields/formatters\";\nimport { cookie } from \"@web/core/browser/cookie\";\n\nexport class AceField extends Component {\n    static template = \"web.AceField\";\n    static props = {\n        ...standardFieldProps,\n        mode: { type: String, optional: true },\n    };\n    static defaultProps = {\n        mode: \"qweb\",\n    };\n    static components = { CodeEditor };\n\n    setup() {\n        this.state = useState({});\n        useRecordObserver((record) => {\n            this.state.initialValue = formatText(record.data[this.props.name]);\n        });\n\n        this.isDirty = false;\n\n        const { model } = this.props.record;\n        useBus(model.bus, \"WILL_SAVE_URGENTLY\", () => this.commitChanges());\n        useBus(model.bus, \"NEED_LOCAL_CHANGES\", ({ detail }) =>\n            detail.proms.push(this.commitChanges())\n        );\n    }\n\n    get mode() {\n        return this.props.mode === \"xml\" ? \"qweb\" : this.props.mode;\n    }\n    get theme() {\n        return cookie.get(\"color_scheme\") === \"dark\" ? \"monokai\" : \"\";\n    }\n\n    handleChange(editedValue) {\n        if (this.state.initialValue !== editedValue) {\n            this.isDirty = true;\n        } else {\n            this.isDirty = false;\n        }\n        this.props.record.model.bus.trigger(\"FIELD_IS_DIRTY\", this.isDirty);\n        this.editedValue = editedValue;\n    }\n\n    async commitChanges() {\n        if (!this.props.readonly && this.isDirty) {\n            if (this.state.initialValue !== this.editedValue) {\n                await this.props.record.update({ [this.props.name]: this.editedValue });\n            }\n            this.isDirty = false;\n            this.props.record.model.bus.trigger(\"FIELD_IS_DIRTY\", false);\n        }\n    }\n}\n\nexport const aceField = {\n    component: AceField,\n    displayName: _t(\"Ace Editor\"),\n    supportedOptions: [\n        {\n            label: _t(\"Mode\"),\n            name: \"mode\",\n            type: \"string\",\n        },\n    ],\n    supportedTypes: [\"text\", \"html\"],\n    extractProps: ({ options }) => ({\n        mode: options.mode,\n    }),\n};\n\nregistry.category(\"fields\").add(\"ace\", aceField);\nregistry.category(\"fields\").add(\"code\", aceField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nimport { Component } from \"@odoo/owl\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nexport class AttachmentImageField extends Component {\n    static template = \"web.AttachmentImageField\";\n    static props = { ...standardFieldProps };\n}\n\nexport const attachmentImageField = {\n    component: AttachmentImageField,\n    displayName: _t(\"Attachment Image\"),\n    supportedTypes: [\"many2one\"],\n};\n\nregistry.category(\"fields\").add(\"attachment_image\", attachmentImageField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nimport { Component } from \"@odoo/owl\";\nconst formatters = registry.category(\"formatters\");\n\nexport class BadgeField extends Component {\n    static template = \"web.BadgeField\";\n    static props = {\n        ...standardFieldProps,\n        decorations: { type: Object, optional: true },\n    };\n    static defaultProps = {\n        decorations: {},\n    };\n\n    get formattedValue() {\n        const formatter = formatters.get(this.props.record.fields[this.props.name].type);\n        return formatter(this.props.record.data[this.props.name], {\n            selection: this.props.record.fields[this.props.name].selection,\n        });\n    }\n\n    get classFromDecoration() {\n        const evalContext = this.props.record.evalContextWithVirtualIds;\n        for (const decorationName in this.props.decorations) {\n            if (evaluateBooleanExpr(this.props.decorations[decorationName], evalContext)) {\n                // fallback case for text-bg-muted\n                if (decorationName === \"muted\") {\n                    return \"text-bg-300\";\n                }\n                return `text-bg-${decorationName}`;\n            }\n        }\n        return \"text-bg-300\";\n    }\n}\n\nexport const badgeField = {\n    component: BadgeField,\n    displayName: _t(\"Badge\"),\n    supportedTypes: [\"selection\", \"many2one\", \"char\"],\n    extractProps: ({ decorations }) => {\n        return { decorations };\n    },\n};\n\nregistry.category(\"fields\").add(\"badge\", badgeField);\n", "import { Component } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { getFieldDomain } from \"@web/model/relational_model/utils\";\nimport { useSpecialData } from \"@web/views/fields/relational_utils\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nexport class BadgeSelectionField extends Component {\n    static template = \"web.BadgeSelectionField\";\n    static props = {\n        ...standardFieldProps,\n        domain: { type: [Array, Function], optional: true },\n        size: {\n            type: String,\n            optional: true,\n            validate: (s) => [\"sm\", \"md\", \"lg\"].includes(s),\n            default: \"md\",\n        },\n    };\n\n    setup() {\n        this.type = this.props.record.fields[this.props.name].type;\n        if (this.type === \"many2one\") {\n            this.specialData = useSpecialData((orm, props) => {\n                const domain = getFieldDomain(props.record, props.name, props.domain);\n                const { relation } = props.record.fields[props.name];\n                return orm.call(relation, \"name_search\", [\"\", domain]);\n            });\n        }\n    }\n\n    get options() {\n        switch (this.type) {\n            case \"many2one\":\n                return this.specialData.data;\n            case \"selection\":\n                return this.props.record.fields[this.props.name].selection;\n            default:\n                return [];\n        }\n    }\n\n    get string() {\n        switch (this.type) {\n            case \"many2one\":\n                return this.props.record.data[this.props.name]\n                    ? this.props.record.data[this.props.name][1]\n                    : \"\";\n            case \"selection\":\n                return this.props.record.data[this.props.name] !== false\n                    ? this.options.find((o) => o[0] === this.props.record.data[this.props.name])[1]\n                    : \"\";\n            default:\n                return \"\";\n        }\n    }\n    get value() {\n        const rawValue = this.props.record.data[this.props.name];\n        return this.type === \"many2one\" && rawValue ? rawValue[0] : rawValue;\n    }\n\n    stringify(value) {\n        return JSON.stringify(value);\n    }\n\n    /**\n     * @param {string | number | false} value\n     */\n    onChange(value) {\n        switch (this.type) {\n            case \"many2one\":\n                if (value === false) {\n                    this.props.record.update({ [this.props.name]: false });\n                } else {\n                    this.props.record.update({\n                        [this.props.name]: this.options.find((option) => option[0] === value),\n                    });\n                }\n                break;\n            case \"selection\":\n                if (value === this.value) {\n                    const { required } = this.props.record.fields[this.props.name];\n                    if (!required) {\n                        this.props.record.update({ [this.props.name]: false });\n                    }\n                } else {\n                    this.props.record.update({ [this.props.name]: value });\n                }\n                break;\n        }\n    }\n}\n\nexport const badgeSelectionField = {\n    component: BadgeSelectionField,\n    displayName: _t(\"Badges\"),\n    supportedTypes: [\"many2one\", \"selection\"],\n    supportedOptions: [\n        {\n            label: \"Size\",\n            name: \"size\",\n            type: \"selection\",\n            choices: [\n                { label: \"Small\", value: \"sm\" },\n                { label: \"Medium\", value: \"md\" },\n                { label: \"Large\", value: \"lg\" },\n            ],\n            default: \"md\",\n        },\n    ],\n    isEmpty: (record, fieldName) => record.data[fieldName] === false,\n    extractProps: (fieldInfo, dynamicInfo) => ({\n        domain: dynamicInfo.domain,\n        size: fieldInfo.options.size,\n    }),\n};\n\nregistry.category(\"fields\").add(\"selection_badge\", badgeSelectionField);\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { isBinarySize, toBase64Length } from \"@web/core/utils/binary\";\nimport { download } from \"@web/core/network/download\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { FileUploader } from \"../file_handler\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport const MAX_FILENAME_SIZE_BYTES = 0xFF;  // filenames do not exceed 255 bytes on Linux/Windows/MacOS\n\nexport class BinaryField extends Component {\n    static template = \"web.BinaryField\";\n    static components = {\n        FileUploader,\n    };\n    static props = {\n        ...standardFieldProps,\n        acceptedFileExtensions: { type: String, optional: true },\n        fileNameField: { type: String, optional: true },\n    };\n    static defaultProps = {\n        acceptedFileExtensions: \"*\",\n    };\n\n    setup() {\n        this.notification = useService(\"notification\");\n    }\n\n    get fileName() {\n        return (\n            this.props.record.data[this.props.fileNameField] ||\n            this.props.record.data[this.props.name] ||\n            \"\"\n        ).slice(0, toBase64Length(MAX_FILENAME_SIZE_BYTES));\n    }\n\n    update({ data, name }) {\n        const { fileNameField, record } = this.props;\n        const changes = { [this.props.name]: data || false };\n        if (fileNameField in record.fields && record.data[fileNameField] !== name) {\n            changes[fileNameField] = name || '';\n        }\n        return this.props.record.update(changes);\n    }\n\n    getDownloadData() {\n        return {\n            model: this.props.record.resModel,\n            id: this.props.record.resId,\n            field: this.props.name,\n            filename_field: this.fileName,\n            filename: this.fileName || \"\",\n            download: true,\n            data: isBinarySize(this.props.record.data[this.props.name])\n                ? null\n                : this.props.record.data[this.props.name],\n        };\n    }\n\n    async onFileDownload() {\n        await download({\n            data: this.getDownloadData(),\n            url: \"/web/content\",\n        });\n    }\n}\n\nexport class ListBinaryField extends BinaryField {\n    static template = \"web.ListBinaryField\";\n}\n\nexport const binaryField = {\n    component: BinaryField,\n    displayName: _t(\"File\"),\n    supportedOptions: [\n        {\n            label: _t(\"Accepted file extensions\"),\n            name: \"accepted_file_extensions\",\n            type: \"string\",\n        },\n    ],\n    supportedTypes: [\"binary\"],\n    extractProps: ({ attrs, options }) => ({\n        acceptedFileExtensions: options.accepted_file_extensions,\n        fileNameField: attrs.filename,\n    }),\n};\n\nexport const listBinaryField = {\n    ...binaryField,\n    component: ListBinaryField,\n};\n\nregistry.category(\"fields\").add(\"binary\", binaryField);\nregistry.category(\"fields\").add(\"list.binary\", listBinaryField);\n", "import { Component, useState } from \"@odoo/owl\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nexport class BooleanField extends Component {\n    static template = \"web.BooleanField\";\n    static components = { CheckBox };\n    static props = {\n        ...standardFieldProps,\n    };\n\n    setup() {\n        this.state = useState({});\n        useRecordObserver((record) => {\n            this.state.value = record.data[this.props.name];\n        });\n    }\n\n    /**\n     * @param {boolean} newValue\n     */\n    onChange(newValue) {\n        this.state.value = newValue;\n        this.props.record.update({ [this.props.name]: newValue });\n    }\n}\n\nexport const booleanField = {\n    component: BooleanField,\n    displayName: _t(\"Checkbox\"),\n    supportedTypes: [\"boolean\"],\n    isEmpty: () => false,\n};\n\nregistry.category(\"fields\").add(\"boolean\", booleanField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class BooleanFavoriteField extends Component {\n    static template = \"web.BooleanFavoriteField\";\n    static props = {\n        ...standardFieldProps,\n        noLabel: { type: Boolean, optional: true },\n        autosave: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        noLabel: false,\n    };\n\n    get iconClass() {\n        return this.props.record.data[this.props.name] ? \"fa fa-star me-1\" : \"fa fa-star-o me-1\";\n    }\n\n    get label() {\n        return this.props.record.data[this.props.name]\n            ? _t(\"Remove from Favorites\")\n            : _t(\"Add to Favorites\");\n    }\n\n    async update() {\n        if (this.props.readonly) {\n            return;\n        }\n        const changes = { [this.props.name]: !this.props.record.data[this.props.name] };\n        await this.props.record.update(changes, { save: this.props.autosave });\n    }\n}\n\nexport const booleanFavoriteField = {\n    component: BooleanFavoriteField,\n    displayName: _t(\"Favorite\"),\n    supportedTypes: [\"boolean\"],\n    isEmpty: () => false,\n    listViewWidth: ({ hasLabel }) => (!hasLabel ? 20 : false),\n    supportedOptions: [\n        {\n            label: _t(\"Autosave\"),\n            name: \"autosave\",\n            type: \"boolean\",\n            default: true,\n            help: _t(\n                \"If checked, the record will be saved immediately when the field is modified.\"\n            ),\n        },\n    ],\n    extractProps: ({ attrs, options }, dynamicInfo) => ({\n        noLabel: exprToBoolean(attrs.nolabel),\n        autosave: \"autosave\" in options ? Boolean(options.autosave) : true,\n        readonly: dynamicInfo.readonly,\n    }),\n};\n\nregistry.category(\"fields\").add(\"boolean_favorite\", booleanFavoriteField);\n", "import { Component } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nexport class BooleanIconField extends Component {\n    static template = \"web.BooleanIconField\";\n    static props = {\n        ...standardFieldProps,\n        icon: { type: String, optional: true },\n        label: { type: String, optional: true },\n    };\n    static defaultProps = {\n        icon: \"fa-check-square-o\",\n    };\n\n    update() {\n        this.props.record.update({ [this.props.name]: !this.props.record.data[this.props.name] });\n    }\n}\n\nexport const booleanIconField = {\n    component: BooleanIconField,\n    displayName: _t(\"Boolean Icon\"),\n    supportedOptions: [\n        {\n            label: _t(\"Icon\"),\n            name: \"icon\",\n            type: \"string\",\n        },\n    ],\n    supportedTypes: [\"boolean\"],\n    extractProps: ({ options, string }) => ({\n        icon: options.icon,\n        label: string,\n    }),\n};\n\nregistry.category(\"fields\").add(\"boolean_icon\", booleanIconField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { booleanField, BooleanField } from \"../boolean/boolean_field\";\n\nexport class BooleanToggleField extends BooleanField {\n    static template = \"web.BooleanToggleField\";\n    static props = {\n        ...BooleanField.props,\n        autosave: { type: Boolean, optional: true },\n    };\n\n    async onChange(newValue) {\n        this.state.value = newValue;\n        const changes = { [this.props.name]: newValue };\n        await this.props.record.update(changes, { save: this.props.autosave });\n    }\n}\n\nexport const booleanToggleField = {\n    ...booleanField,\n    component: BooleanToggleField,\n    displayName: _t(\"Toggle\"),\n    supportedOptions: [\n        {\n            label: _t(\"Autosave\"),\n            name: \"autosave\",\n            type: \"boolean\",\n            default: true,\n            help: _t(\n                \"If checked, the record will be saved immediately when the field is modified.\"\n            ),\n        },\n    ],\n    extractProps({ options }, dynamicInfo) {\n        return {\n            autosave: \"autosave\" in options ? Boolean(options.autosave) : true,\n            readonly: dynamicInfo.readonly,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"boolean_toggle\", booleanToggleField);\n", "import { registry } from \"@web/core/registry\";\nimport { booleanToggleField, BooleanToggleField } from \"./boolean_toggle_field\";\n\nexport class ListBooleanToggleField extends BooleanToggleField {\n    static template = \"web.ListBooleanToggleField\";\n\n    async onClick() {\n        if (!this.props.readonly && this.props.record.isInEdition) {\n            const changes = { [this.props.name]: !this.props.record.data[this.props.name] };\n            await this.props.record.update(changes, { save: this.props.autosave });\n        }\n    }\n}\n\nexport const listBooleanToggleField = {\n    ...booleanToggleField,\n    component: ListBooleanToggleField,\n};\n\nregistry.category(\"fields\").add(\"list.boolean_toggle\", listBooleanToggleField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { useDynamicPlaceholder } from \"../dynamic_placeholder_hook\";\nimport { formatChar } from \"../formatters\";\nimport { useInputField } from \"../input_field_hook\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { TranslationButton } from \"../translation_button\";\n\nimport { Component, useEffect, useExternalListener, useRef } from \"@odoo/owl\";\n\nexport class CharField extends Component {\n    static template = \"web.CharField\";\n    static components = {\n        TranslationButton,\n    };\n    static props = {\n        ...standardFieldProps,\n        autocomplete: { type: String, optional: true },\n        isPassword: { type: Boolean, optional: true },\n        placeholder: { type: String, optional: true },\n        dynamicPlaceholder: { type: Boolean, optional: true },\n        dynamicPlaceholderModelReferenceField: { type: String, optional: true },\n        placeholderField: { type: String, optional: true },\n    };\n    static defaultProps = { dynamicPlaceholder: false };\n\n    setup() {\n        this.input = useRef(\"input\");\n        if (this.props.dynamicPlaceholder) {\n            this.dynamicPlaceholder = useDynamicPlaceholder(this.input);\n            useExternalListener(document, \"keydown\", this.dynamicPlaceholder.onKeydown);\n            useEffect(() =>\n                this.dynamicPlaceholder.updateModel(\n                    this.props.dynamicPlaceholderModelReferenceField\n                )\n            );\n        }\n        useInputField({\n            getValue: () => this.props.record.data[this.props.name] || \"\",\n            parse: (v) => this.parse(v),\n        });\n\n        this.selectionStart = this.props.record.data[this.props.name]?.length || 0;\n    }\n\n    get shouldTrim() {\n        return this.props.record.fields[this.props.name].trim && !this.props.isPassword;\n    }\n    get maxLength() {\n        return this.props.record.fields[this.props.name].size;\n    }\n    get isTranslatable() {\n        return this.props.record.fields[this.props.name].translate;\n    }\n    get formattedValue() {\n        return formatChar(this.props.record.data[this.props.name], {\n            isPassword: this.props.isPassword,\n        });\n    }\n    get hasDynamicPlaceholder() {\n        return this.props.dynamicPlaceholder && !this.props.readonly;\n    }\n\n    get placeholder() {\n        return this.props.record.data[this.props.placeholderField] || this.props.placeholder;\n    }\n\n    parse(value) {\n        if (this.shouldTrim) {\n            return value.trim();\n        }\n        return value;\n    }\n\n    onBlur() {\n        this.selectionStart = this.input.el.selectionStart;\n    }\n\n    async onDynamicPlaceholderOpen() {\n        await this.dynamicPlaceholder.open({\n            validateCallback: this.onDynamicPlaceholderValidate.bind(this),\n        });\n    }\n\n    async onDynamicPlaceholderValidate(chain, defaultValue) {\n        if (chain) {\n            this.input.el.focus();\n            const dynamicPlaceholder = ` {{object.${chain}${\n                defaultValue?.length ? ` ||| ${defaultValue}` : \"\"\n            }}}`;\n            this.input.el.setRangeText(\n                dynamicPlaceholder,\n                this.selectionStart,\n                this.selectionStart,\n                \"end\"\n            );\n            // trigger events to make the field dirty\n            this.input.el.dispatchEvent(new InputEvent(\"input\"));\n            this.input.el.dispatchEvent(new KeyboardEvent(\"keydown\"));\n            this.input.el.focus();\n        }\n    }\n}\n\nexport const charField = {\n    component: CharField,\n    displayName: _t(\"Text\"),\n    supportedTypes: [\"char\"],\n    supportedOptions: [\n        {\n            label: _t(\"Dynamic Placeholder\"),\n            name: \"placeholder_field\",\n            type: \"field\",\n            availableTypes: [\"char\"],\n            help: _t(\n                \"Displays the value of the selected field as a textual hint. If the selected field is empty, the static placeholder attribute is displayed instead.\"\n            ),\n        },\n    ],\n    extractProps: ({ attrs, options }) => ({\n        isPassword: exprToBoolean(attrs.password),\n        dynamicPlaceholder: options.dynamic_placeholder || false,\n        dynamicPlaceholderModelReferenceField:\n            options.dynamic_placeholder_model_reference_field || \"\",\n        autocomplete: attrs.autocomplete,\n        placeholder: attrs.placeholder,\n        placeholderField: options.placeholder_field,\n    }),\n};\n\nregistry.category(\"fields\").add(\"char\", charField);\n", "import { Component } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nexport class ColorField extends Component {\n    static template = \"web.ColorField\";\n    static props = {\n        ...standardFieldProps,\n    };\n\n    get color() {\n        return this.props.record.data[this.props.name] || \"\";\n    }\n}\n\nexport const colorField = {\n    component: ColorField,\n    supportedTypes: [\"char\"],\n    extractProps(fieldInfo, dynamicInfo) {\n        return {\n            readonly: dynamicInfo.readonly,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"color\", colorField);\n", "import { ColorList } from \"@web/core/colorlist/colorlist\";\nimport { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class ColorPickerField extends Component {\n    static template = \"web.ColorPickerField\";\n    static components = {\n        ColorList,\n    };\n    static props = {\n        ...standardFieldProps,\n        canToggle: { type: Boolean },\n    };\n\n    static RECORD_COLORS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];\n\n    get isExpanded() {\n        return !this.props.canToggle && !this.props.readonly;\n    }\n\n    switchColor(colorIndex) {\n        this.props.record.update({ [this.props.name]: colorIndex });\n    }\n}\n\nexport const colorPickerField = {\n    component: ColorPickerField,\n    supportedTypes: [\"integer\"],\n    extractProps: ({ viewType }) => ({\n        canToggle: viewType !== \"list\",\n    }),\n};\n\nregistry.category(\"fields\").add(\"color_picker\", colorPickerField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { registry } from \"@web/core/registry\";\nimport { omit } from \"@web/core/utils/objects\";\n\nimport { CopyButton } from \"@web/core/copy_button/copy_button\";\nimport { CharField } from \"../char/char_field\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { UrlField } from \"../url/url_field\";\n\nimport { Component } from \"@odoo/owl\";\n\nclass CopyClipboardField extends Component {\n    static template = \"web.CopyClipboardField\";\n    static props = {\n        ...standardFieldProps,\n        string: { type: String, optional: true },\n        disabledExpr: { type: String, optional: true },\n    };\n\n    setup() {\n        this.copyText = this.props.string || _t(\"Copy\");\n        this.successText = _t(\"Copied\");\n    }\n\n    get copyButtonClassName() {\n        return `o_btn_${this.type}_copy btn-sm`;\n    }\n    get fieldProps() {\n        return omit(this.props, \"string\", \"disabledExpr\");\n    }\n    get type() {\n        return this.props.record.fields[this.props.name].type;\n    }\n    get disabled() {\n        return this.props.disabledExpr\n            ? evaluateBooleanExpr(\n                  this.props.disabledExpr,\n                  this.props.record.evalContextWithVirtualIds\n              )\n            : false;\n    }\n}\n\nexport class CopyClipboardButtonField extends CopyClipboardField {\n    static template = \"web.CopyClipboardButtonField\";\n    static components = { CopyButton };\n\n    get copyButtonClassName() {\n        return `o_btn_${this.type}_copy btn-primary rounded-2`;\n    }\n}\n\nexport class CopyClipboardCharField extends CopyClipboardField {\n    static components = { Field: CharField, CopyButton };\n\n    get copyButtonIcon() {\n        return \"fa-clone\";\n    }\n}\n\nexport class CopyClipboardURLField extends CopyClipboardField {\n    static components = { Field: UrlField, CopyButton };\n\n    get copyButtonIcon() {\n        return \"fa-link\";\n    }\n}\n\n// ----------------------------------------------------------------------------\n\nfunction extractProps({ attrs }) {\n    return {\n        string: attrs.string,\n        disabledExpr: attrs.disabled,\n    };\n}\n\nexport const copyClipboardButtonField = {\n    component: CopyClipboardButtonField,\n    displayName: _t(\"Copy to Clipboard\"),\n    extractProps,\n};\n\nregistry.category(\"fields\").add(\"CopyClipboardButton\", copyClipboardButtonField);\n\nexport const copyClipboardCharField = {\n    component: CopyClipboardCharField,\n    displayName: _t(\"Copy Text to Clipboard\"),\n    supportedTypes: [\"char\"],\n    extractProps,\n};\n\nregistry.category(\"fields\").add(\"CopyClipboardChar\", copyClipboardCharField);\n\nexport const copyClipboardURLField = {\n    component: CopyClipboardURLField,\n    displayName: _t(\"Copy URL to Clipboard\"),\n    supportedTypes: [\"char\"],\n    extractProps,\n};\n\nregistry.category(\"fields\").add(\"CopyClipboardURL\", copyClipboardURLField);\n", "import { Component, onWillRender, useState } from \"@odoo/owl\";\nimport { useDateTimePicker } from \"@web/core/datetime/datetime_hook\";\nimport { areDatesEqual, deserializeDate, deserializeDateTime, today } from \"@web/core/l10n/dates\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { ensureArray } from \"@web/core/utils/arrays\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { formatDate, formatDateTime } from \"../formatters\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\n/**\n * @typedef {luxon.DateTime} DateTime\n *\n * @typedef {import(\"../standard_field_props\").StandardFieldProps & {\n *  endDateField?: string;\n *  maxDate?: string;\n *  minDate?: string;\n *  placeholder?: string;\n *  required?: boolean;\n *  rounding?: number;\n *  startDateField?: string;\n *  warnFuture?: boolean;\n *  showSeconds?: boolean;\n *  showTime?: boolean;\n *  minPrecision?: string;\n *  maxPrecision?: string;\n * }} DateTimeFieldProps\n *\n * @typedef {import(\"@web/core/datetime/datetime_picker\").DateTimePickerProps} DateTimePickerProps\n */\n\n/** @extends {Component<DateTimeFieldProps>} */\nexport class DateTimeField extends Component {\n    static props = {\n        ...standardFieldProps,\n        endDateField: { type: String, optional: true },\n        maxDate: { type: String, optional: true },\n        minDate: { type: String, optional: true },\n        alwaysRange: { type: Boolean, optional: true },\n        placeholder: { type: String, optional: true },\n        required: { type: Boolean, optional: true },\n        rounding: { type: Number, optional: true },\n        startDateField: { type: String, optional: true },\n        warnFuture: { type: Boolean, optional: true },\n        showSeconds: { type: Boolean, optional: true },\n        showTime: { type: Boolean, optional: true },\n        minPrecision: {\n            type: String,\n            optional: true,\n            validate: (props) => [\"days\", \"months\", \"years\", \"decades\"].includes(props),\n        },\n        maxPrecision: {\n            type: String,\n            optional: true,\n            validate: (props) => [\"days\", \"months\", \"years\", \"decades\"].includes(props),\n        },\n        condensed: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        showSeconds: true,\n        showTime: true,\n    };\n\n    static template = \"web.DateTimeField\";\n\n    //-------------------------------------------------------------------------\n    // Getters\n    //-------------------------------------------------------------------------\n\n    get endDateField() {\n        return this.relatedField ? this.props.endDateField || this.props.name : null;\n    }\n\n    get field() {\n        return this.props.record.fields[this.props.name];\n    }\n\n    get relatedField() {\n        return this.props.startDateField || this.props.endDateField;\n    }\n\n    get startDateField() {\n        return this.props.startDateField || this.props.name;\n    }\n\n    get values() {\n        return ensureArray(this.state.value);\n    }\n\n    //-------------------------------------------------------------------------\n    // Lifecycle\n    //-------------------------------------------------------------------------\n\n    setup() {\n        const getPickerProps = () => {\n            const value = this.getRecordValue();\n            /** @type {DateTimePickerProps} */\n            const pickerProps = {\n                value,\n                type: this.field.type,\n                range: this.isRange(value),\n            };\n            if (this.props.maxDate) {\n                pickerProps.maxDate = this.parseLimitDate(this.props.maxDate);\n            }\n            if (this.props.minDate) {\n                pickerProps.minDate = this.parseLimitDate(this.props.minDate);\n            }\n            if (!isNaN(this.props.rounding)) {\n                pickerProps.rounding = this.props.rounding;\n            } else if (!this.props.showSeconds) {\n                pickerProps.rounding = 0;\n            }\n            if (this.props.maxPrecision) {\n                pickerProps.maxPrecision = this.props.maxPrecision;\n            }\n            if (this.props.minPrecision) {\n                pickerProps.minPrecision = this.props.minPrecision;\n            }\n            return pickerProps;\n        };\n\n        const dateTimePicker = useDateTimePicker({\n            target: \"root\",\n            showSeconds: this.props.showSeconds,\n            condensed: this.props.condensed,\n            get pickerProps() {\n                return getPickerProps();\n            },\n            onChange: () => {\n                this.state.range = this.isRange(this.state.value);\n            },\n            onApply: () => {\n                const toUpdate = {};\n                if (Array.isArray(this.state.value)) {\n                    // Value is already a range\n                    [toUpdate[this.startDateField], toUpdate[this.endDateField]] = this.state.value;\n                } else {\n                    toUpdate[this.props.name] = this.state.value;\n                }\n\n                // If startDateField or endDateField are not set, delete unchanged fields\n                for (const fieldName in toUpdate) {\n                    if (areDatesEqual(toUpdate[fieldName], this.props.record.data[fieldName])) {\n                        delete toUpdate[fieldName];\n                    }\n                }\n\n                if (Object.keys(toUpdate).length) {\n                    this.props.record.update(toUpdate);\n                }\n            },\n        });\n        // Subscribes to changes made on the picker state\n        this.state = useState(dateTimePicker.state);\n        this.openPicker = dateTimePicker.open;\n\n        onWillRender(() => this.triggerIsDirty());\n    }\n\n    //-------------------------------------------------------------------------\n    // Methods\n    //-------------------------------------------------------------------------\n\n    /**\n     * @param {number} valueIndex\n     */\n    async addDate(valueIndex) {\n        const values = this.values;\n        values[valueIndex] = valueIndex\n            ? values[0].plus({ hours: 1 })\n            : values[1].minus({ hours: 1 });\n\n        this.state.focusedDateIndex = valueIndex;\n        this.state.value = values;\n        this.state.range = true;\n\n        this.openPicker(valueIndex);\n    }\n\n    /**\n     * @param {number} valueIndex\n     */\n    getFormattedValue(valueIndex) {\n        const value = this.values[valueIndex];\n        const { condensed, showSeconds, showTime } = this.props;\n        return value\n            ? this.field.type === \"date\"\n                ? formatDate(value, { condensed })\n                : formatDateTime(value, { condensed, showSeconds, showTime })\n            : \"\";\n    }\n\n    /**\n     * @returns {DateTimePickerProps[\"value\"]}\n     */\n    getRecordValue() {\n        if (this.relatedField) {\n            return [\n                this.props.record.data[this.startDateField],\n                this.props.record.data[this.endDateField],\n            ];\n        } else {\n            return this.props.record.data[this.props.name];\n        }\n    }\n\n    /**\n     * @param {number} index\n     */\n    isDateInTheFuture(index) {\n        return this.values[index] > today();\n    }\n\n    /**\n     * @param {string} fieldName\n     */\n    isEmpty(fieldName) {\n        return fieldName === this.startDateField ? !this.values[0] : !this.values[1];\n    }\n\n    /**\n     * @param {DateTimePickerProps[\"value\"]} value\n     * @returns {boolean}\n     */\n    isRange(value) {\n        if (!this.relatedField) {\n            return false;\n        }\n        return (\n            this.props.alwaysRange ||\n            this.props.required ||\n            ensureArray(value).filter(Boolean).length === 2\n        );\n    }\n\n    /**\n     * @param {string} value\n     */\n    parseLimitDate(value) {\n        if (value === \"today\") {\n            return value;\n        }\n        return this.field.type === \"date\" ? deserializeDate(value) : deserializeDateTime(value);\n    }\n\n    /**\n     * @return {boolean}\n     */\n    shouldShowSeparator() {\n        return (\n            (this.props.alwaysRange &&\n                (this.props.readonly\n                    ? !this.isEmpty(this.startDateField) || !this.isEmpty(this.endDateField)\n                    : true)) ||\n            (this.state.range &&\n                (this.props.required ||\n                    (!this.isEmpty(this.startDateField) && !this.isEmpty(this.endDateField))))\n        );\n    }\n\n    /**\n     * The given props are used to compute the current value and compare it to\n     * the state handled by the datetime hook.\n     *\n     * @param {boolean} [isDirty]\n     */\n    triggerIsDirty(isDirty) {\n        this.props.record.model.bus.trigger(\n            \"FIELD_IS_DIRTY\",\n            isDirty ?? !areDatesEqual(this.getRecordValue(), this.state.value)\n        );\n    }\n\n    //-------------------------------------------------------------------------\n    // Handlers\n    //-------------------------------------------------------------------------\n\n    onInput() {\n        this.triggerIsDirty(true);\n    }\n}\n\nconst START_DATE_FIELD_OPTION = \"start_date_field\";\nconst END_DATE_FIELD_OPTION = \"end_date_field\";\n\nexport const dateField = {\n    component: DateTimeField,\n    displayName: _t(\"Date\"),\n    supportedOptions: [\n        {\n            label: _t(\"Earliest accepted date\"),\n            name: \"min_date\",\n            type: \"string\",\n            help: _t(`ISO-formatted date (e.g. \"2018-12-31\") or \"%s\".`, \"today\"),\n        },\n        {\n            label: _t(\"Latest accepted date\"),\n            name: \"max_date\",\n            type: \"string\",\n            help: _t(`ISO-formatted date (e.g. \"2018-12-31\") or \"%s\".`, \"today\"),\n        },\n        {\n            label: _t(\"Warning for future dates\"),\n            name: \"warn_future\",\n            type: \"boolean\",\n            help: _t(`Displays a warning icon if the input dates are in the future.`),\n        },\n        {\n            label: _t(\"Minimal precision\"),\n            name: \"min_precision\",\n            type: \"selection\",\n            help: _t(\n                `Choose which minimal precision (days, months, ...) you want in the datetime picker.`\n            ),\n            choices: [\n                { label: _t(\"Days\"), value: \"days\" },\n                { label: _t(\"Months\"), value: \"months\" },\n                { label: _t(\"Years\"), value: \"years\" },\n                { label: _t(\"Decades\"), value: \"decades\" },\n            ],\n        },\n        {\n            label: _t(\"Maximal precision\"),\n            name: \"max_precision\",\n            type: \"selection\",\n            help: _t(\n                `Choose which maximal precision (days, months, ...) you want in the datetime picker.`\n            ),\n            choices: [\n                { label: _t(\"Days\"), value: \"days\" },\n                { label: _t(\"Months\"), value: \"months\" },\n                { label: _t(\"Years\"), value: \"years\" },\n                { label: _t(\"Decades\"), value: \"decades\" },\n            ],\n        },\n        {\n            label: _t(\"Condensed display\"),\n            name: \"condensed\",\n            type: \"boolean\",\n            help: _t(`Set to true to display days, months (and hours) with unpadded numbers`),\n        },\n    ],\n    supportedTypes: [\"date\"],\n    extractProps: ({ attrs, options }, dynamicInfo) => ({\n        endDateField: options[END_DATE_FIELD_OPTION],\n        maxDate: options.max_date,\n        minDate: options.min_date,\n        alwaysRange: exprToBoolean(options.always_range),\n        placeholder: attrs.placeholder,\n        required: dynamicInfo.required,\n        rounding: options.rounding && parseInt(options.rounding, 10),\n        startDateField: options[START_DATE_FIELD_OPTION],\n        warnFuture: exprToBoolean(options.warn_future),\n        minPrecision: options.min_precision,\n        maxPrecision: options.max_precision,\n        condensed: options.condensed,\n    }),\n    fieldDependencies: ({ type, attrs, options }) => {\n        const deps = [];\n        if (options[START_DATE_FIELD_OPTION]) {\n            deps.push({\n                name: options[START_DATE_FIELD_OPTION],\n                type,\n                readonly: false,\n                ...attrs,\n            });\n            if (options[END_DATE_FIELD_OPTION]) {\n                console.warn(\n                    `A field cannot have both ${START_DATE_FIELD_OPTION} and ${END_DATE_FIELD_OPTION} options at the same time`\n                );\n            }\n        } else if (options[END_DATE_FIELD_OPTION]) {\n            deps.push({\n                name: options[END_DATE_FIELD_OPTION],\n                type,\n                readonly: false,\n                ...attrs,\n            });\n        }\n        return deps;\n    },\n};\n\nexport const dateTimeField = {\n    ...dateField,\n    displayName: _t(\"Date & Time\"),\n    supportedOptions: [\n        ...dateField.supportedOptions,\n        {\n            label: _t(\"Time interval\"),\n            name: \"rounding\",\n            type: \"number\",\n            default: 5,\n            help: _t(\n                `Control the number of minutes in the time selection. E.g. set it to 15 to work in quarters.`\n            ),\n        },\n        {\n            label: _t(\"Show seconds\"),\n            name: \"show_seconds\",\n            type: \"boolean\",\n            default: true,\n            help: _t(`Displays or hides the seconds in the datetime value.`),\n        },\n        {\n            label: _t(\"Show time\"),\n            name: \"show_time\",\n            type: \"boolean\",\n            default: true,\n            help: _t(`Displays or hides the time in the datetime value.`),\n        },\n    ],\n    extractProps: ({ attrs, options }, dynamicInfo) => ({\n        ...dateField.extractProps({ attrs, options }, dynamicInfo),\n        showSeconds: exprToBoolean(options.show_seconds ?? true),\n        showTime: exprToBoolean(options.show_time ?? true),\n    }),\n    supportedTypes: [\"datetime\"],\n};\n\nexport const dateRangeField = {\n    ...dateTimeField,\n    displayName: _t(\"Date Range\"),\n    supportedOptions: [\n        ...dateTimeField.supportedOptions,\n        {\n            label: _t(\"Start date field\"),\n            name: START_DATE_FIELD_OPTION,\n            type: \"field\",\n            availableTypes: [\"date\", \"datetime\"],\n        },\n        {\n            label: _t(\"End date field\"),\n            name: END_DATE_FIELD_OPTION,\n            type: \"field\",\n            availableTypes: [\"date\", \"datetime\"],\n        },\n        {\n            label: _t(\"Always range\"),\n            name: \"always_range\",\n            type: \"boolean\",\n            default: false,\n            help: _t(\n                `Set to true the full range input has to be display by default, even if empty.`\n            ),\n        },\n    ],\n    supportedTypes: [\"date\", \"datetime\"],\n    listViewWidth: ({ type }) => (type === \"datetime\" ? 294 : 180),\n    isValid: (record, fieldname, fieldInfo) => {\n        if (fieldInfo.widget === \"daterange\") {\n            if (\n                !record.data[fieldInfo.options[END_DATE_FIELD_OPTION]] !==\n                    !record.data[fieldname] &&\n                evaluateBooleanExpr(\n                    record.activeFields[fieldInfo.options[END_DATE_FIELD_OPTION]]?.required,\n                    record.evalContextWithVirtualIds\n                )\n            ) {\n                return false;\n            }\n            if (\n                !record.data[fieldInfo.options[START_DATE_FIELD_OPTION]] !==\n                    !record.data[fieldname] &&\n                evaluateBooleanExpr(\n                    record.activeFields[fieldInfo.options[START_DATE_FIELD_OPTION]]?.required,\n                    record.evalContextWithVirtualIds\n                )\n            ) {\n                return false;\n            }\n        }\n        return !record.isFieldInvalid(fieldname);\n    },\n};\n\nregistry\n    .category(\"fields\")\n    .add(\"date\", dateField)\n    .add(\"daterange\", dateRangeField)\n    .add(\"datetime\", dateTimeField);\n", "import { registry } from \"@web/core/registry\";\nimport { DateTimeField, dateField, dateRangeField, dateTimeField } from \"./datetime_field\";\n\nexport class ListDateTimeField extends DateTimeField {\n    /**\n     * @override\n     */\n    shouldShowSeparator() {\n        return this.props.readonly\n            ? this.relatedField && this.values.some(Boolean)\n            : super.shouldShowSeparator();\n    }\n}\n\nexport const listDateField = { ...dateField, component: ListDateTimeField };\nexport const listDateRangeField = { ...dateRangeField, component: ListDateTimeField };\nexport const listDateTimeField = { ...dateTimeField, component: ListDateTimeField };\n\nregistry\n    .category(\"fields\")\n    .add(\"list.date\", listDateField)\n    .add(\"list.daterange\", listDateRangeField)\n    .add(\"list.datetime\", listDateTimeField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Component, useState } from \"@odoo/owl\";\nimport { Domain, InvalidDomainError } from \"@web/core/domain\";\nimport { DomainSelector } from \"@web/core/domain_selector/domain_selector\";\nimport { DomainSelectorDialog } from \"@web/core/domain_selector_dialog/domain_selector_dialog\";\nimport { EvaluationError } from \"@web/core/py_js/py_builtin\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { SelectCreateDialog } from \"@web/views/view_dialogs/select_create_dialog\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { useBus, useService, useOwnedDialogs } from \"@web/core/utils/hooks\";\nimport { useGetTreeDescription, useMakeGetFieldDef } from \"@web/core/tree_editor/utils\";\nimport { useGetDefaultLeafDomain } from \"@web/core/domain_selector/utils\";\nimport { treeFromDomain } from \"@web/core/tree_editor/condition_tree\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\n\nexport class DomainField extends Component {\n    static template = \"web.DomainField\";\n    static components = {\n        DomainSelector,\n    };\n    static props = {\n        ...standardFieldProps,\n        context: { type: Object, optional: true },\n        editInDialog: { type: Boolean, optional: true },\n        resModel: { type: String, optional: true },\n        isFoldable: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        editInDialog: false,\n        isFoldable: false,\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.getDomainTreeDescription = useGetTreeDescription();\n        this.makeGetFieldDef = useMakeGetFieldDef();\n        this.getDefaultLeafDomain = useGetDefaultLeafDomain();\n        this.addDialog = useOwnedDialogs();\n\n        this.state = useState({\n            isValid: null,\n            recordCount: null,\n            folded: this.props.isFoldable,\n            facets: [],\n        });\n\n        this.debugDomain = null;\n        useRecordObserver(async (record, nextProps) => {\n            nextProps = { ...nextProps, record };\n            if (this.debugDomain && this.props.readonly !== nextProps.readonly) {\n                this.debugDomain = null;\n            }\n            if (this.debugDomain) {\n                this.state.isValid = await this.quickValidityCheck(nextProps);\n                if (!this.state.isValid) {\n                    this.state.recordCount = 0;\n                    nextProps.record.setInvalidField(nextProps.name);\n                }\n            } else {\n                this.checkProps(nextProps); // not awaited\n            }\n            if (nextProps.isFoldable) {\n                this.loadFacets(nextProps);\n            }\n        });\n\n        useBus(this.props.record.model.bus, \"NEED_LOCAL_CHANGES\", async (ev) => {\n            if (this.debugDomain) {\n                const props = this.props;\n                const handleChanges = async () => {\n                    await props.record.update({ [props.name]: this.debugDomain });\n                    const isValid = await this.quickValidityCheck(props);\n                    if (isValid) {\n                        this.debugDomain = null; // will allow the count to be loaded if needed\n                    } else {\n                        this.state.isValid = false;\n                        this.state.recordCount = 0;\n                        props.record.setInvalidField(props.name);\n                    }\n                };\n                ev.detail.proms.push(handleChanges());\n            }\n        });\n    }\n\n    getContext(props = this.props) {\n        return props.context;\n    }\n\n    getDomain(props = this.props) {\n        return props.record.data[props.name] || \"[]\";\n    }\n\n    getEvaluatedDomain(props = this.props) {\n        const domainStringRepr = this.getDomain(props);\n        const evalContext = this.getContext(props);\n        try {\n            const domain = new Domain(domainStringRepr).toList(evalContext);\n            // Here, there is still some incertitude on the domain validity.\n            // we could improve this check but a complete (async) check is done\n            // when loading the record count associated with the domain.\n            return domain;\n        } catch (error) {\n            if (error instanceof InvalidDomainError || error instanceof EvaluationError) {\n                return { isInvalid: true };\n            }\n            throw error;\n        }\n    }\n\n    getResModel(props = this.props) {\n        let resModel = props.resModel;\n        if (props.record.fieldNames.includes(resModel)) {\n            resModel = props.record.data[resModel];\n        }\n        return resModel;\n    }\n\n    async addCondition() {\n        const defaultDomain = await this.getDefaultLeafDomain(this.getResModel());\n        this.update(defaultDomain);\n        this.state.folded = false;\n    }\n\n    async loadFacets(props = this.props) {\n        const resModel = this.getResModel(props);\n\n        if (!resModel) {\n            this.state.facets = [];\n            this.state.folded = false;\n            return;\n        }\n\n        if (typeof resModel !== \"string\") {\n            // we don't want to support invalid models\n            throw new Error(`Invalid model: ${resModel}`);\n        }\n\n        let promises = [];\n        const domain = this.getDomain(props);\n        try {\n            const getFieldDef = await this.makeGetFieldDef(resModel, treeFromDomain(domain));\n            const tree = treeFromDomain(domain, { distributeNot: !this.env.debug, getFieldDef });\n            const trees = !tree.negate && tree.value === \"&\" ? tree.children : [tree];\n            promises = trees.map((tree) => this.getDomainTreeDescription(resModel, tree));\n        } catch (error) {\n            if (error.data?.name === \"builtins.KeyError\" && error.data.message === resModel) {\n                // we don't want to support invalid models\n                throw new Error(`Invalid model: ${resModel}`);\n            }\n            this.state.facets = [];\n            this.state.folded = false;\n        }\n        this.state.facets = await Promise.all(promises);\n    }\n\n    async checkProps(props = this.props) {\n        const resModel = this.getResModel(props);\n        if (!resModel) {\n            this.updateState({});\n            return;\n        }\n\n        if (typeof resModel !== \"string\") {\n            // we don't want to support invalid models\n            throw new Error(`Invalid model: ${resModel}`);\n        }\n\n        const domain = this.getEvaluatedDomain(props);\n        if (domain.isInvalid) {\n            this.updateState({ isValid: false, recordCount: 0 });\n            return;\n        }\n\n        let recordCount;\n        const context = this.getContext(props);\n        try {\n            recordCount = await this.orm.silent.searchCount(resModel, domain, { context });\n        } catch (error) {\n            if (error.data?.name === \"builtins.KeyError\" && error.data.message === resModel) {\n                // we don't want to support invalid models\n                throw new Error(`Invalid model: ${resModel}`);\n            }\n            this.updateState({ isValid: false, recordCount: 0 });\n            return;\n        }\n\n        this.updateState({ isValid: true, recordCount });\n    }\n\n    onButtonClick() {\n        // resModel, domain, and context are assumed to be valid here.\n        this.addDialog(\n            SelectCreateDialog,\n            {\n                title: _t(\"Selected records\"),\n                noCreate: true,\n                multiSelect: false,\n                resModel: this.getResModel(),\n                domain: this.getEvaluatedDomain(),\n                context: this.getContext(),\n            },\n            {\n                // The counter is reloaded \"on close\" because some modal allows\n                // to modify data that can impact the counter\n                onClose: () => this.checkProps(),\n            }\n        );\n    }\n\n    onEditDialogBtnClick() {\n        // resModel is assumed to be valid here\n        this.addDialog(DomainSelectorDialog, {\n            resModel: this.getResModel(),\n            domain: this.getDomain(),\n            isDebugMode: !!this.env.debug,\n            onConfirm: this.update.bind(this),\n        });\n    }\n\n    async quickValidityCheck(props) {\n        const resModel = this.getResModel(props);\n        if (!resModel) {\n            return false;\n        }\n        const domain = this.getEvaluatedDomain(props);\n        if (domain.isInvalid) {\n            return false;\n        }\n        return rpc(\"/web/domain/validate\", { model: resModel, domain });\n    }\n\n    update(domain, isDebugEdited = false) {\n        if (!isDebugEdited) {\n            this.debugDomain = null;\n        }\n        this.props.record.update({ [this.props.name]: domain });\n        this.props.record.model.bus.trigger(\"FIELD_IS_DIRTY\", false);\n    }\n\n    debugUpdate(domain) {\n        const isDirty = domain !== this.getDomain();\n        this.debugDomain = isDirty ? domain : null;\n        this.props.record.model.bus.trigger(\"FIELD_IS_DIRTY\", isDirty);\n        if (!this.props.record.isValid) {\n            this.props.record.resetFieldValidity(this.props.name);\n        }\n    }\n\n    fold() {\n        this.state.folded = true;\n    }\n\n    updateState(params = {}) {\n        Object.assign(this.state, {\n            isValid: \"isValid\" in params ? params.isValid : null,\n            recordCount: \"recordCount\" in params ? params.recordCount : null,\n        });\n    }\n}\n\nexport const domainField = {\n    component: DomainField,\n    displayName: _t(\"Domain\"),\n    supportedOptions: [\n        {\n            label: _t(\"Edit in dialog\"),\n            name: \"in_dialog\",\n            type: \"boolean\",\n        },\n        {\n            label: _t(\"Foldable\"),\n            name: \"foldable\",\n            type: \"boolean\",\n            help: _t(\"Display the domain using facets\"),\n        },\n        {\n            label: _t(\"Model\"),\n            name: \"model\",\n            type: \"string\",\n        },\n    ],\n    supportedTypes: [\"char\"],\n    isEmpty: () => false,\n    extractProps({ options }, dynamicInfo) {\n        return {\n            editInDialog: options.in_dialog,\n            isFoldable: options.foldable,\n            resModel: options.model,\n            context: dynamicInfo.context,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"domain\", domainField);\n", "import { usePopover } from \"@web/core/popover/popover_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useComponent } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { DynamicPlaceholderPopover } from \"./dynamic_placeholder_popover\";\n\nexport function useDynamicPlaceholder(elementRef) {\n    const TRIGGER_KEY = \"#\";\n    const ownerField = useComponent();\n    const triggerKeyReplaceRegex = new RegExp(`${TRIGGER_KEY}$`);\n    let closeCallback;\n    let positionCallback;\n    const popover = usePopover(DynamicPlaceholderPopover, {\n        onClose: () => closeCallback?.(),\n        onPositioned: (popper, position) => positionCallback?.(popper, position),\n    });\n    const notification = useService(\"notification\");\n\n    let model = null;\n\n    const onDynamicPlaceholderValidate = function (path, defaultValue) {\n        const element = elementRef?.el;\n        if (!element) {\n            return;\n        }\n        let rangeIndex = parseInt(element.getAttribute(\"data-oe-dynamic-placeholder-range-index\"));\n        // When the user cancel/close the popover, the path is empty.\n        if (path) {\n            defaultValue = defaultValue.replace(\"|||\", \"\");\n            const dynamicPlaceholder = ` {{object.${path}${\n                defaultValue?.length ? ` ||| ${defaultValue}` : \"\"\n            }}}`;\n            const baseValue = element.value;\n            const splitedValue = [baseValue.slice(0, rangeIndex), baseValue.slice(rangeIndex)];\n            const newValue =\n                splitedValue[0].replace(triggerKeyReplaceRegex, \"\") +\n                dynamicPlaceholder +\n                splitedValue[1];\n            const changes = { [ownerField.props.name]: newValue };\n            ownerField.props.record.update(changes);\n            element.value = newValue;\n\n            // -1 to take the removal of the trigger key char into account\n            rangeIndex += dynamicPlaceholder.length - 1;\n            element.setSelectionRange(rangeIndex, rangeIndex);\n            element.removeAttribute(\"data-oe-dynamic-placeholder-range-index\");\n        }\n    };\n    const onDynamicPlaceholderClose = function () {\n        elementRef?.el.focus();\n    };\n\n    /**\n     * Open a Model Field Selector which can select fields to create a dynamic\n     * placeholder string in the Input with or without a default text value.\n     *\n     * @public\n     * @param {Object} opts\n     * @param {function} opts.validateCallback\n     * @param {function} opts.closeCallback\n     * @param {function} [opts.positionCallback]\n     */\n    async function open(opts) {\n        if (!model) {\n            return notification.add(\n                _t(\"You need to select a model before opening the dynamic placeholder selector.\"),\n                { type: \"danger\" }\n            );\n        }\n        closeCallback = opts.closeCallback;\n        positionCallback = opts.positionCallback;\n        popover.open(elementRef?.el, {\n            resModel: model,\n            validate: opts.validateCallback,\n        });\n    }\n    async function onKeydown(ev) {\n        const element = elementRef?.el;\n        if (ev.target === element && ev.key === TRIGGER_KEY) {\n            const currentRangeIndex = element.selectionStart;\n            // +1 to take the trigger key char into account\n            element.setAttribute(\"data-oe-dynamic-placeholder-range-index\", currentRangeIndex + 1);\n            await open({\n                validateCallback: onDynamicPlaceholderValidate,\n                closeCallback: onDynamicPlaceholderClose,\n            });\n        }\n    }\n    function updateModel(model_name_location) {\n        const recordData = ownerField.props.record.data;\n        model = recordData[model_name_location] || recordData.model;\n    }\n\n    return {\n        updateModel: updateModel,\n        onKeydown: onKeydown,\n        setElementRef: (er) => (elementRef = er),\n        open: open,\n    };\n}\n", "import { memoize } from \"@web/core/utils/functions\";\nimport { useAutofocus, useService } from \"@web/core/utils/hooks\";\nimport { ModelFieldSelectorPopover } from \"@web/core/model_field_selector/model_field_selector_popover\";\nimport { Component, onWillStart, useState } from \"@odoo/owl\";\nimport { user } from \"@web/core/user\";\n\nconst allowedQwebExpressions = memoize(async (model, orm) => {\n    return await orm.call(model, \"mail_allowed_qweb_expressions\");\n});\n\nexport class DynamicPlaceholderPopover extends Component {\n    static template = \"web.DynamicPlaceholderPopover\";\n    static components = {\n        ModelFieldSelectorPopover,\n    };\n    static props = [\"resModel\", \"validate\", \"close\"];\n\n    setup() {\n        useAutofocus();\n        this.state = useState({\n            path: \"\",\n            isPathSelected: false,\n            defaultValue: \"\",\n        });\n        this.orm = useService(\"orm\");\n\n        onWillStart(async () => {\n            [this.isTemplateEditor, this.allowedQwebExpressions] = await Promise.all([\n                user.hasGroup(\"mail.group_mail_template_editor\"),\n                // (only the first element is the cache key)\n                allowedQwebExpressions(this.props.resModel, this.orm),\n            ]);\n        });\n    }\n\n    filter(fieldDef, path) {\n        const fullPath = `object${path ? `.${path}` : \"\"}.${fieldDef.name}`;\n        if (!this.isTemplateEditor && !this.allowedQwebExpressions.includes(fullPath)) {\n            return false;\n        }\n        return ![\"one2many\", \"boolean\", \"many2many\"].includes(fieldDef.type) && fieldDef.searchable;\n    }\n    closeFieldSelector(isPathSelected = false) {\n        if (isPathSelected) {\n            this.state.isPathSelected = true;\n            return;\n        }\n        this.props.close();\n    }\n    setPath(path, fieldInfo) {\n        this.state.path = path;\n        this.state.fieldName = fieldInfo?.string;\n    }\n    setDefaultValue(value) {\n        this.state.defaultValue = value;\n    }\n    validate() {\n        this.props.validate(this.state.path, this.state.defaultValue);\n        this.props.close();\n    }\n\n    onBack() {\n        this.state.defaultValue = \"\";\n        this.state.isPathSelected = false;\n        this.state.path = \"\";\n    }\n\n    // @TODO should rework this to use hotkeys\n    async onInputKeydown(ev) {\n        switch (ev.key) {\n            case \"Enter\": {\n                this.validate();\n                ev.stopPropagation();\n                ev.preventDefault();\n                break;\n            }\n            case \"Escape\": {\n                this.props.close();\n                break;\n            }\n        }\n    }\n}\n", "import { ModelFieldSelector } from \"@web/core/model_field_selector/model_field_selector\";\n\nexport class DynamicModelFieldSelector extends ModelFieldSelector {\n\n    static props = {\n        ...ModelFieldSelector.props,\n        record: { type: Object, optional: true },\n        recordProps: { type: Object, optional: true },\n    };\n}\n", "import { registry } from \"@web/core/registry\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { CharField, charField } from \"@web/views/fields/char/char_field\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { DynamicModelFieldSelector } from \"./dynamic_model_field_selector\";\n\nexport class DynamicModelFieldSelectorChar extends CharField {\n    static template = \"web.DynamicModelFieldSelectorChar\";\n    static components = {\n        ...CharField.components,\n        DynamicModelFieldSelector,\n    };\n\n    static props = {\n        ...CharField.props,\n        resModel: { type: String, optional: true },\n        onlySearchable: { type: Boolean, optional: true },\n        followRelations: { type: Boolean, optional: true },\n    };\n\n    /**\n     * Update record\n     *\n     * @param {string} value\n     * @private\n     */\n    async _onRecordUpdate(value) {\n        await this.props.record.update({ [this.props.name]: value });\n    }\n\n    //---- Getters ----\n    get getSelectorProps() {\n        return {\n            path: this.props.record.data[this.props.name],\n            resModel: this.getResModel(),\n            readonly: this.props.readonly,\n            record: this.props.record,\n            recordProps: this.props,\n            update: this._onRecordUpdate.bind(this),\n            isDebugMode: !!this.env.debug,\n            filter: this.filter.bind(this),\n            followRelations: this.props.followRelations,\n        };\n    }\n\n    filter(fieldDef) {\n        return !this.props.onlySearchable || fieldDef.searchable;\n    }\n\n    getResModel(props = this.props) {\n        const resModel = props.record.data[props.resModel];\n        if (!resModel) {\n            return props.record.resModel;\n        }\n        return resModel;\n    }\n}\n\nexport const dynamicModelFieldSelectorChar = {\n    ...charField,\n    component: DynamicModelFieldSelectorChar,\n    supportedOptions: [\n        {\n            label: _t(\"Follow relations\"),\n            name: \"follow_relations\",\n            type: \"boolean\",\n            default: true,\n        },\n        {\n            label: _t(\"Model\"),\n            name: \"model\",\n            type: \"string\",\n        },\n        {\n            label: _t(\"Only searchable\"),\n            name: \"only_searchable\",\n            type: \"string\",\n        },\n    ],\n    extractProps({ options }, dynamicInfo) {\n        return {\n            followRelations: options.follow_relations ?? true,\n            onlySearchable: exprToBoolean(options.only_searchable),\n            resModel: options.model,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"DynamicModelFieldSelectorChar\", dynamicModelFieldSelectorChar);\n", "import { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useInputField } from \"../input_field_hook\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class EmailField extends Component {\n    static template = \"web.EmailField\";\n    static props = {\n        ...standardFieldProps,\n        placeholder: { type: String, optional: true },\n    };\n\n    setup() {\n        useInputField({ getValue: () => this.props.record.data[this.props.name] || \"\" });\n    }\n}\n\nexport const emailField = {\n    component: EmailField,\n    displayName: _t(\"Email\"),\n    supportedTypes: [\"char\"],\n    extractProps: ({ attrs }) => ({\n        placeholder: attrs.placeholder,\n    }),\n};\n\nregistry.category(\"fields\").add(\"email\", emailField);\n\nclass FormEmailField extends EmailField {\n    static template = \"web.FormEmailField\";\n}\n\nexport const formEmailField = {\n    ...emailField,\n    component: FormEmailField,\n};\n\nregistry.category(\"fields\").add(\"form.email\", formEmailField);\n", "import { Domain } from \"@web/core/domain\";\nimport { evaluateBooleanExpr, evaluateExpr } from \"@web/core/py_js/py\";\nimport { registry } from \"@web/core/registry\";\nimport { utils } from \"@web/core/ui/ui_service\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { getFieldContext } from \"@web/model/relational_model/utils\";\nimport { X2M_TYPES, getClassNameFromDecoration } from \"@web/views/utils\";\nimport { getTooltipInfo } from \"./field_tooltip\";\n\nimport { Component, xml } from \"@odoo/owl\";\n\nconst isSmall = utils.isSmall;\n\nconst viewRegistry = registry.category(\"views\");\nconst fieldRegistry = registry.category(\"fields\");\n\nconst supportedInfoValidation = {\n    type: Array,\n    element: Object,\n    shape: {\n        label: String,\n        name: String,\n        type: String,\n        availableTypes: { type: Array, element: String, optional: true },\n        default: { type: String, optional: true },\n        help: { type: String, optional: true },\n        choices: /* choices if type == selection */ {\n            type: Array,\n            element: Object,\n            shape: { label: String, value: String },\n            optional: true,\n        },\n    },\n    optional: true,\n};\n\nfieldRegistry.addValidation({\n    component: { validate: (c) => c.prototype instanceof Component },\n    displayName: { type: String, optional: true },\n    supportedAttributes: supportedInfoValidation,\n    supportedOptions: supportedInfoValidation,\n    supportedTypes: { type: Array, element: String, optional: true },\n    extractProps: { type: Function, optional: true },\n    isEmpty: { type: Function, optional: true },\n    isValid: { type: Function, optional: true }, // Override the validation for the validation visual feedbacks\n    additionalClasses: { type: Array, element: String, optional: true },\n    fieldDependencies: {\n        type: [Function, { type: Array, element: Object, shape: { name: String, type: String } }],\n        optional: true,\n    },\n    relatedFields: {\n        type: [\n            Function,\n            {\n                type: Array,\n                element: Object,\n                shape: {\n                    name: String,\n                    type: String,\n                    readonly: { type: Boolean, optional: true },\n                    selection: { type: Array, element: { type: Array, element: String } },\n                    optional: true,\n                },\n            },\n        ],\n        optional: true,\n    },\n    useSubView: { type: Boolean, optional: true },\n    label: { type: [String, { value: false }], optional: true },\n    listViewWidth: {\n        type: [\n            Number,\n            {\n                type: Array,\n                element: Number,\n                validate: (array) => array.length === 1 || array.length === 2,\n            },\n            Function,\n        ],\n        optional: true,\n    },\n});\n\nclass DefaultField extends Component {\n    static template = xml``;\n    static props = [\"*\"];\n}\n\nexport function getFieldFromRegistry(fieldType, widget, viewType, jsClass) {\n    const prefixes = jsClass ? [jsClass, viewType, \"\"] : [viewType, \"\"];\n    const findInRegistry = (key) => {\n        for (const prefix of prefixes) {\n            const _key = prefix ? `${prefix}.${key}` : key;\n            if (fieldRegistry.contains(_key)) {\n                return fieldRegistry.get(_key);\n            }\n        }\n    };\n    if (widget) {\n        const field = findInRegistry(widget);\n        if (field) {\n            return field;\n        }\n        console.warn(`Missing widget: ${widget} for field of type ${fieldType}`);\n    }\n    return findInRegistry(fieldType) || { component: DefaultField };\n}\n\nexport function fieldVisualFeedback(field, record, fieldName, fieldInfo) {\n    const readonly = evaluateBooleanExpr(fieldInfo.readonly, record.evalContextWithVirtualIds);\n    const required = evaluateBooleanExpr(fieldInfo.required, record.evalContextWithVirtualIds);\n    const inEdit = record.isInEdition;\n\n    let empty = !record.isNew;\n    if (\"isEmpty\" in field) {\n        empty = empty && field.isEmpty(record, fieldName);\n    } else {\n        empty = empty && !record.data[fieldName];\n    }\n    empty = inEdit ? empty && readonly : empty;\n    return {\n        readonly,\n        required,\n        invalid: field.isValid\n            ? !field.isValid(record, fieldName, fieldInfo)\n            : record.isFieldInvalid(fieldName),\n        empty,\n    };\n}\n\nexport function getPropertyFieldInfo(propertyField) {\n    const { name, relatedPropertyField, string, type } = propertyField;\n\n    const fieldInfo = {\n        name,\n        string,\n        type,\n        widget: type,\n        options: {},\n        column_invisible: \"False\",\n        invisible: \"False\",\n        readonly: \"False\",\n        required: \"False\",\n        attrs: {},\n        relatedPropertyField,\n\n        // ??? We don t use it ? But it s in the fieldInfo of the field\n        context: \"{}\",\n        help: undefined,\n        onChange: false,\n        forceSave: false,\n        decorations: {},\n        // ???\n    };\n\n    if (type === \"many2one\" || type === \"many2many\") {\n        const { domain, relation } = propertyField;\n        fieldInfo.relation = relation;\n        fieldInfo.domain = domain;\n\n        if (relation === \"res.users\" || relation === \"res.partner\") {\n            fieldInfo.widget =\n                propertyField.type === \"many2one\" ? \"many2one_avatar\" : \"many2many_tags_avatar\";\n        } else {\n            fieldInfo.widget = propertyField.type === \"many2one\" ? type : \"many2many_tags\";\n        }\n    } else if (type === \"tags\") {\n        fieldInfo.tags = propertyField.tags;\n        fieldInfo.widget = `property_tags`;\n    } else if (type === \"selection\") {\n        fieldInfo.selection = propertyField.selection;\n    }\n\n    fieldInfo.field = getFieldFromRegistry(propertyField.type, fieldInfo.widget);\n    let { relatedFields } = fieldInfo.field;\n    if (relatedFields) {\n        if (relatedFields instanceof Function) {\n            relatedFields = relatedFields({ options: {}, attrs: {} });\n        }\n        fieldInfo.relatedFields = Object.fromEntries(relatedFields.map((f) => [f.name, f]));\n    }\n\n    return fieldInfo;\n}\nexport class Field extends Component {\n    static template = \"web.Field\";\n    static props = [\"fieldInfo?\", \"*\"];\n    static parseFieldNode = function (node, models, modelName, viewType, jsClass) {\n        const name = node.getAttribute(\"name\");\n        const widget = node.getAttribute(\"widget\");\n        const fields = models[modelName].fields;\n        if (!fields[name]) {\n            throw new Error(`\"${modelName}\".\"${name}\" field is undefined.`);\n        }\n        const field = getFieldFromRegistry(fields[name].type, widget, viewType, jsClass);\n        const fieldInfo = {\n            name,\n            type: fields[name].type,\n            viewType,\n            widget,\n            field,\n            context: \"{}\",\n            string: fields[name].string,\n            help: undefined,\n            onChange: false,\n            forceSave: false,\n            options: {},\n            decorations: {},\n            attrs: {},\n            domain: undefined,\n        };\n\n        for (const attr of [\"invisible\", \"column_invisible\", \"readonly\", \"required\"]) {\n            fieldInfo[attr] = node.getAttribute(attr);\n            if (fieldInfo[attr] === \"True\") {\n                if (attr === \"column_invisible\") {\n                    fieldInfo.invisible = \"True\";\n                }\n            } else if (fieldInfo[attr] === null && fields[name][attr]) {\n                fieldInfo[attr] = \"True\";\n            }\n        }\n\n        for (const { name, value } of node.attributes) {\n            if ([\"name\", \"widget\"].includes(name)) {\n                // avoid adding name and widget to attrs\n                continue;\n            }\n            if ([\"context\", \"string\", \"help\", \"domain\"].includes(name)) {\n                fieldInfo[name] = value;\n            } else if (name === \"on_change\") {\n                fieldInfo.onChange = exprToBoolean(value);\n            } else if (name === \"options\") {\n                fieldInfo.options = evaluateExpr(value);\n            } else if (name === \"force_save\") {\n                fieldInfo.forceSave = exprToBoolean(value);\n            } else if (name.startsWith(\"decoration-\")) {\n                // prepare field decorations\n                fieldInfo.decorations[name.replace(\"decoration-\", \"\")] = value;\n            } else if (!name.startsWith(\"t-att\")) {\n                // all other (non dynamic) attributes\n                fieldInfo.attrs[name] = value;\n            }\n        }\n        if (name === \"id\") {\n            fieldInfo.readonly = \"True\";\n        }\n\n        if (widget === \"handle\") {\n            fieldInfo.isHandle = true;\n        }\n\n        if (X2M_TYPES.includes(fields[name].type)) {\n            const views = {};\n            let relatedFields = fieldInfo.field.relatedFields;\n            if (relatedFields) {\n                if (relatedFields instanceof Function) {\n                    relatedFields = relatedFields(fieldInfo);\n                }\n                for (const relatedField of relatedFields) {\n                    if (!(\"readonly\" in relatedField)) {\n                        relatedField.readonly = true;\n                    }\n                }\n                relatedFields = Object.fromEntries(relatedFields.map((f) => [f.name, f]));\n                views.default = { fieldNodes: relatedFields, fields: relatedFields };\n                if (!fieldInfo.field.useSubView) {\n                    fieldInfo.viewMode = \"default\";\n                }\n            }\n            for (const child of node.children) {\n                const viewType = child.tagName;\n                const { ArchParser } = viewRegistry.get(viewType);\n                // We copy and hence isolate the subview from the main view's tree\n                // This way, the subview's tree is autonomous and CSS selectors will work normally\n                const childCopy = child.cloneNode(true);\n                const archInfo = new ArchParser().parse(childCopy, models, fields[name].relation);\n                views[viewType] = {\n                    ...archInfo,\n                    limit: archInfo.limit || 40,\n                    fields: models[fields[name].relation].fields,\n                };\n            }\n\n            let viewMode = node.getAttribute(\"mode\");\n            if (viewMode) {\n                if (viewMode.split(\",\").length !== 1) {\n                    viewMode = isSmall() ? \"kanban\" : \"list\";\n                }\n            } else {\n                if (views.list && !views.kanban) {\n                    viewMode = \"list\";\n                } else if (!views.list && views.kanban) {\n                    viewMode = \"kanban\";\n                } else if (views.list && views.kanban) {\n                    viewMode = isSmall() ? \"kanban\" : \"list\";\n                }\n            }\n            if (viewMode) {\n                fieldInfo.viewMode = viewMode;\n            }\n            if (Object.keys(views).length) {\n                fieldInfo.relatedFields = models[fields[name].relation]?.fields;\n                fieldInfo.views = views;\n            }\n        }\n        if (fields[name].type === \"many2one_reference\") {\n            let relatedFields = fieldInfo.field.relatedFields;\n            if (relatedFields) {\n                relatedFields = Object.fromEntries(relatedFields.map((f) => [f.name, f]));\n                fieldInfo.viewMode = \"default\";\n                fieldInfo.views = {\n                    default: { fieldNodes: relatedFields, fields: relatedFields },\n                };\n            }\n        }\n\n        return fieldInfo;\n    };\n\n    setup() {\n        if (this.props.fieldInfo) {\n            this.field = this.props.fieldInfo.field;\n        } else {\n            const fieldType = this.props.record.fields[this.props.name].type;\n            this.field = getFieldFromRegistry(fieldType, this.props.type);\n        }\n    }\n\n    get classNames() {\n        const { class: _class, fieldInfo, name, record } = this.props;\n        const { readonly, required, invalid, empty } = fieldVisualFeedback(\n            this.field,\n            record,\n            name,\n            fieldInfo || {}\n        );\n        const classNames = {\n            o_field_widget: true,\n            o_readonly_modifier: readonly,\n            o_required_modifier: required,\n            o_field_invalid: invalid,\n            o_field_empty: empty,\n            [`o_field_${this.type}`]: true,\n            [_class]: Boolean(_class),\n        };\n        if (this.field.additionalClasses) {\n            for (const cls of this.field.additionalClasses) {\n                classNames[cls] = true;\n            }\n        }\n\n        // generate field decorations classNames (only if field-specific decorations\n        // have been defined in an attribute, e.g. decoration-danger=\"other_field = 5\")\n        // only handle the text-decoration.\n        if (fieldInfo && fieldInfo.decorations) {\n            const { decorations } = fieldInfo;\n            for (const decoName in decorations) {\n                const value = evaluateBooleanExpr(\n                    decorations[decoName],\n                    record.evalContextWithVirtualIds\n                );\n                classNames[getClassNameFromDecoration(decoName)] = value;\n            }\n        }\n\n        return classNames;\n    }\n\n    get type() {\n        return this.props.type || this.props.record.fields[this.props.name].type;\n    }\n\n    get fieldComponentProps() {\n        const record = this.props.record;\n        let readonly = this.props.readonly || false;\n\n        let propsFromNode = {};\n        if (this.props.fieldInfo) {\n            let fieldInfo = this.props.fieldInfo;\n            readonly =\n                readonly ||\n                evaluateBooleanExpr(fieldInfo.readonly, record.evalContextWithVirtualIds);\n\n            if (this.field.extractProps) {\n                if (this.props.attrs) {\n                    fieldInfo = {\n                        ...fieldInfo,\n                        attrs: { ...fieldInfo.attrs, ...this.props.attrs },\n                    };\n                }\n\n                const dynamicInfo = {\n                    get context() {\n                        return getFieldContext(record, fieldInfo.name, fieldInfo.context);\n                    },\n                    domain() {\n                        const evalContext = record.evalContext;\n                        if (fieldInfo.domain) {\n                            return new Domain(evaluateExpr(fieldInfo.domain, evalContext)).toList();\n                        }\n                    },\n                    required: evaluateBooleanExpr(\n                        fieldInfo.required,\n                        record.evalContextWithVirtualIds\n                    ),\n                    readonly: readonly,\n                };\n                propsFromNode = this.field.extractProps(fieldInfo, dynamicInfo);\n            }\n        }\n\n        const props = { ...this.props };\n        delete props.style;\n        delete props.class;\n        delete props.showTooltip;\n        delete props.fieldInfo;\n        delete props.attrs;\n        delete props.type;\n        delete props.readonly;\n\n        return {\n            readonly: readonly || !record.isInEdition || false,\n            ...propsFromNode,\n            ...props,\n        };\n    }\n\n    get tooltip() {\n        if (this.props.showTooltip) {\n            const tooltip = getTooltipInfo({\n                field: this.props.record.fields[this.props.name],\n                fieldInfo: this.props.fieldInfo || {},\n            });\n            if (Boolean(odoo.debug) || (tooltip && JSON.parse(tooltip).field.help)) {\n                return tooltip;\n            }\n        }\n        return false;\n    }\n}\n", "export function getTooltipInfo(params) {\n    let widgetDescription = undefined;\n    if (params.fieldInfo.widget) {\n        widgetDescription = params.fieldInfo.field.displayName;\n    }\n\n    const info = {\n        viewMode: params.viewMode,\n        resModel: params.resModel,\n        debug: Boolean(odoo.debug),\n        field: {\n            name: params.field.name,\n            label: params.field.string,\n            help: params.fieldInfo.help ?? params.field.help,\n            type: params.field.type,\n            widget: params.fieldInfo.widget,\n            widgetDescription,\n            context: params.fieldInfo.context,\n            domain: params.fieldInfo.domain || params.field.domain,\n            invisible: params.fieldInfo.invisible,\n            column_invisible: params.fieldInfo.column_invisible,\n            readonly: params.fieldInfo.readonly,\n            required: params.fieldInfo.required,\n            changeDefault: params.field.change_default,\n            relation: params.field.relation,\n            model_field: params.field.model_field,\n            selection: params.field.selection,\n            default: params.field.default,\n        },\n    };\n    return JSON.stringify(info);\n}\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        checkSize: { 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        checkSize: true,\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 (this.props.checkSize && !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 { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useInputField } from \"../input_field_hook\";\nimport { useNumpadDecimal } from \"../numpad_decimal_hook\";\nimport { formatFloat } from \"../formatters\";\nimport { parseFloat } from \"../parsers\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class FloatField extends Component {\n    static template = \"web.FloatField\";\n    static props = {\n        ...standardFieldProps,\n        formatNumber: { type: Boolean, optional: true },\n        inputType: { type: String, optional: true },\n        step: { type: Number, optional: true },\n        digits: { type: Array, optional: true },\n        placeholder: { type: String, optional: true },\n        humanReadable: { type: Boolean, optional: true },\n        decimals: { type: Number, optional: true },\n    };\n    static defaultProps = {\n        formatNumber: true,\n        inputType: \"text\",\n        humanReadable: false,\n        decimals: 0,\n    };\n\n    setup() {\n        this.state = useState({\n            hasFocus: false,\n        });\n        this.inputRef = useInputField({\n            getValue: () => this.formattedValue,\n            refName: \"numpadDecimal\",\n            parse: (v) => this.parse(v),\n        });\n        useNumpadDecimal();\n    }\n\n    onFocusIn() {\n        this.state.hasFocus = true;\n    }\n\n    onFocusOut() {\n        this.state.hasFocus = false;\n    }\n\n    parse(value) {\n        return this.props.inputType === \"number\" ? Number(value) : parseFloat(value);\n    }\n\n    get formattedValue() {\n        if (\n            !this.props.formatNumber ||\n            (this.props.inputType === \"number\" && !this.props.readonly && this.value)\n        ) {\n            return this.value;\n        }\n        const options = {\n            digits: this.props.digits,\n            field: this.props.record.fields[this.props.name],\n        };\n        if (this.props.humanReadable && !this.state.hasFocus) {\n            return formatFloat(this.value, {\n                ...options,\n                humanReadable: true,\n                decimals: this.props.decimals,\n            });\n        } else {\n            return formatFloat(this.value, { ...options, humanReadable: false });\n        }\n    }\n\n    get value() {\n        return this.props.record.data[this.props.name];\n    }\n}\n\nexport const floatField = {\n    component: FloatField,\n    displayName: _t(\"Float\"),\n    supportedOptions: [\n        {\n            label: _t(\"Format number\"),\n            name: \"enable_formatting\",\n            type: \"boolean\",\n            help: _t(\n                \"Format the value according to your language setup - e.g. thousand separators, rounding, etc.\"\n            ),\n            default: true,\n        },\n        {\n            label: _t(\"Digits\"),\n            name: \"digits\",\n            type: \"digits\",\n        },\n        {\n            label: _t(\"Type\"),\n            name: \"type\",\n            type: \"string\",\n        },\n        {\n            label: _t(\"Step\"),\n            name: \"step\",\n            type: \"number\",\n        },\n        {\n            label: _t(\"User-friendly format\"),\n            name: \"human_readable\",\n            type: \"boolean\",\n            help: _t(\"Use a human readable format (e.g.: 500G instead of 500,000,000,000).\"),\n        },\n        {\n            label: _t(\"Decimals\"),\n            name: \"decimals\",\n            type: \"number\",\n            default: 0,\n            help: _t(\"Use it with the 'User-friendly format' option to customize the formatting.\"),\n        },\n    ],\n    supportedTypes: [\"float\"],\n    isEmpty: () => false,\n    extractProps: ({ 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\n        return {\n            formatNumber:\n                options?.enable_formatting !== undefined\n                    ? Boolean(options.enable_formatting)\n                    : true,\n            inputType: options.type,\n            humanReadable: !!options.human_readable,\n            step: options.step,\n            digits,\n            placeholder: attrs.placeholder,\n            decimals: options.decimals || 0,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"float\", floatField);\n", "import { registry } from \"@web/core/registry\";\nimport { floatField, FloatField } from \"../float/float_field\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class FloatFactorField extends FloatField {\n    static props = {\n        ...FloatField.props,\n        factor: { type: Number, optional: true },\n    };\n    static defaultProps = {\n        ...FloatField.defaultProps,\n        factor: 1,\n    };\n\n    parse(value) {\n        return super.parse(value) / this.props.factor;\n    }\n\n    get value() {\n        return this.props.record.data[this.props.name] * this.props.factor;\n    }\n}\n\nexport const floatFactorField = {\n    ...floatField,\n    component: FloatFactorField,\n    supportedOptions: [\n        ...floatField.supportedOptions,\n        {\n            label: _t(\"Factor\"),\n            name: \"factor\",\n            type: \"number\",\n        },\n    ],\n    extractProps({ options }) {\n        const props = floatField.extractProps(...arguments);\n        props.factor = options.factor;\n        return props;\n    },\n};\n\nregistry.category(\"fields\").add(\"float_factor\", floatFactorField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { formatFloatTime } from \"../formatters\";\nimport { parseFloatTime } from \"../parsers\";\nimport { useInputField } from \"../input_field_hook\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { useNumpadDecimal } from \"../numpad_decimal_hook\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class FloatTimeField extends Component {\n    static template = \"web.FloatTimeField\";\n    static props = {\n        ...standardFieldProps,\n        inputType: { type: String, optional: true },\n        placeholder: { type: String, optional: true },\n        displaySeconds: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        inputType: \"text\",\n    };\n\n    setup() {\n        this.inputFloatTimeRef = useInputField({\n            getValue: () => this.formattedValue,\n            refName: \"numpadDecimal\",\n            parse: (v) => parseFloatTime(v),\n        });\n        useNumpadDecimal();\n    }\n\n    get formattedValue() {\n        return formatFloatTime(this.props.record.data[this.props.name], {\n            displaySeconds: this.props.displaySeconds,\n        });\n    }\n}\n\nexport const floatTimeField = {\n    component: FloatTimeField,\n    displayName: _t(\"Time\"),\n    supportedOptions: [\n        {\n            label: _t(\"Display seconds\"),\n            name: \"display_seconds\",\n            type: \"boolean\",\n        },\n        {\n            label: _t(\"Type\"),\n            name: \"type\",\n            type: \"string\",\n            default: \"text\",\n        },\n    ],\n    supportedTypes: [\"float\"],\n    isEmpty: () => false,\n    extractProps: ({ attrs, options }) => ({\n        displaySeconds: options.displaySeconds,\n        inputType: options.type,\n        placeholder: attrs.placeholder,\n    }),\n};\n\nregistry.category(\"fields\").add(\"float_time\", floatTimeField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { formatFloatFactor } from \"../formatters\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class FloatToggleField extends Component {\n    static template = \"web.FloatToggleField\";\n    static props = {\n        ...standardFieldProps,\n        digits: { type: Array, optional: true },\n        range: { type: Array, optional: true },\n        factor: { type: Number, optional: true },\n        disableReadOnly: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        range: [0.0, 0.5, 1.0],\n        factor: 1,\n        disableReadOnly: false,\n    };\n\n    // TODO perf issue (because of update round trip)\n    // we probably want to have a state and a useEffect or onWillUpateProps\n    onChange() {\n        let currentIndex = this.props.range.indexOf(\n            this.props.record.data[this.props.name] * this.factor\n        );\n        currentIndex++;\n        if (currentIndex > this.props.range.length - 1) {\n            currentIndex = 0;\n        }\n        this.props.record.update({\n            [this.props.name]: this.props.range[currentIndex] / this.factor,\n        });\n    }\n\n    // This property has been created in order to allow overrides in other modules.\n    get factor() {\n        return this.props.factor;\n    }\n\n    get formattedValue() {\n        return formatFloatFactor(this.props.record.data[this.props.name], {\n            digits: this.props.digits,\n            factor: this.factor,\n            field: this.props.record.fields[this.props.name],\n        });\n    }\n}\n\nexport const floatToggleField = {\n    component: FloatToggleField,\n    supportedOptions: [\n        {\n            label: _t(\"Digits\"),\n            name: \"digits\",\n            type: \"digits\",\n        },\n        {\n            label: _t(\"Type\"),\n            name: \"type\",\n            type: \"string\",\n        },\n        {\n            label: _t(\"Range\"),\n            name: \"range\",\n            type: \"string\",\n        },\n        {\n            label: _t(\"Factor\"),\n            name: \"factor\",\n            type: \"number\",\n        },\n        {\n            label: _t(\"Disable readonly\"),\n            name: \"force_button\",\n            type: \"boolean\",\n        },\n    ],\n    supportedTypes: [\"float\"],\n    isEmpty: () => false,\n    extractProps: ({ 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\n        return {\n            digits,\n            range: options.range,\n            factor: options.factor,\n            disableReadOnly: options.force_button || false,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"float_toggle\", floatToggleField);\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", "import { _t } from \"@web/core/l10n/translation\";\nimport { loadBundle } from \"@web/core/assets\";\nimport { registry } from \"@web/core/registry\";\nimport { formatFloat } from \"@web/views/fields/formatters\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nimport { Component, onWillStart, useEffect, useRef } from \"@odoo/owl\";\n\nexport class GaugeField extends Component {\n    static template = \"web.GaugeField\";\n    static props = {\n        ...standardFieldProps,\n        maxValueField: { type: String, optional: true },\n        maxValue: { type: Number, optional: true },\n        title: { type: String, optional: true },\n    };\n    static defaultProps = {\n        maxValue: 100,\n    };\n\n    setup() {\n        this.chart = null;\n        this.canvasRef = useRef(\"canvas\");\n\n        onWillStart(async () => await loadBundle(\"web.chartjs_lib\"));\n\n        useEffect(() => {\n            this.renderChart();\n            return () => {\n                if (this.chart) {\n                    this.chart.destroy();\n                }\n            };\n        });\n    }\n\n    get title() {\n        return this.props.title || this.props.record.fields[this.props.name].string || \"\";\n    }\n\n    get formattedValue() {\n        return formatFloat(this.props.record.data[this.props.name], {\n            humanReadable: true,\n            decimals: 1,\n        });\n    }\n\n    renderChart() {\n        const gaugeValue = this.props.record.data[this.props.name];\n        let maxValue = this.props.maxValueField ? this.props.record.data[this.props.maxValueField] : this.props.maxValue;\n        maxValue = Math.max(gaugeValue, maxValue);\n        let maxLabel = maxValue;\n        if (gaugeValue === 0 && maxValue === 0) {\n            maxValue = 1;\n            maxLabel = 0;\n        }\n        const config = {\n            type: \"doughnut\",\n            data: {\n                datasets: [\n                    {\n                        data: [gaugeValue, maxValue - gaugeValue],\n                        backgroundColor: [\"#1f77b4\", \"#dddddd\"],\n                        label: this.title,\n                    },\n                ],\n            },\n            options: {\n                circumference: 180,\n                rotation: 270,\n                responsive: true,\n                maintainAspectRatio: false,\n                cutout: \"70%\",\n                layout: {\n                    padding: 5,\n                },\n                plugins: {\n                    title: {\n                        display: true,\n                        text: this.title,\n                        padding: 4,\n                    },\n                    tooltip: {\n                        displayColors: false,\n                        callbacks: {\n                            label: function (tooltipItem) {\n                                if (tooltipItem.dataIndex === 0) {\n                                    return _t(\"Value: %(value)s\", { value: gaugeValue });\n                                }\n                                return _t(\"Max: %(max)s\", { max: maxLabel });\n                            },\n                        },\n                    },\n                },\n                aspectRatio: 2,\n            },\n        };\n        this.chart = new Chart(this.canvasRef.el, config);\n    }\n}\n\nexport const gaugeField = {\n    component: GaugeField,\n    supportedOptions: [\n        {\n            label: _t(\"Title\"),\n            name: \"title\",\n            type: \"string\",\n        },\n        {\n            label: _t(\"Max value field\"),\n            name: \"max_value_field\",\n            type: \"field\",\n            availableTypes: [\"integer\", \"float\"],\n        },\n        {\n            label: _t(\"Max value\"),\n            name: \"max_value\",\n            type: \"string\",\n        },\n    ],\n    extractProps: ({ options }) => ({\n        maxValueField: options.max_field,\n        maxValue: options.max_value,\n        title: options.title,\n    }),\n};\n\nregistry.category(\"fields\").add(\"gauge\", gaugeField);\n", "/** @odoo-module **/\n\nimport { useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { CharField, charField } from \"@web/views/fields/char/char_field\";\n\nexport function getGoogleSlideUrl(value, page) {\n    let url = false;\n    const googleRegExp = /(^https:\\/\\/docs.google.com).*(\\/d\\/e\\/|\\/d\\/)([A-Za-z0-9-_]+)/;\n    const google = value.match(googleRegExp);\n    if (google && google[3]) {\n        url = `https://docs.google.com/presentation${google[2]}${google[3]}/preview?slide=${page}`;\n    }\n    return url;\n}\n\nexport class GoogleSlideViewer extends CharField {\n    static template = \"web.GoogleSlideViewer\";\n    setup() {\n        super.setup();\n        this.notification = useService(\"notification\");\n        this.page = 1;\n        this.state = useState({\n            isValid: true,\n        });\n    }\n\n    get fileName() {\n        return this.state.fileName || this.props.record.data[this.props.name] || \"\";\n    }\n\n    _get_slide_page() {\n        return this.props.record.data[this.props.name + \"_page\"]\n            ? this.props.record.data[this.props.name + \"_page\"]\n            : this.page;\n    }\n\n    get url() {\n        let url = this.props.value;\n        if (this.props.record.data[this.props.name]) {\n            url = getGoogleSlideUrl(\n                this.props.record.data[this.props.name],\n                this._get_slide_page()\n            );\n        }\n        return url;\n    }\n\n    onLoadFailed() {\n        this.state.isValid = false;\n        this.notification.add(_t(\"Could not display the selected spreadsheet\"), { type: \"danger\" });\n    }\n}\n\nexport const googleSlideViewer = {\n    ...charField,\n    component: GoogleSlideViewer,\n    displayName: _t(\"Google Slide Viewer\"),\n};\n\nregistry.category(\"fields\").add(\"embed_viewer\", googleSlideViewer);\n", "import { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class HandleField extends Component {\n    static template = \"web.HandleField\";\n    static props = {\n        ...standardFieldProps,\n    };\n}\n\nexport const handleField = {\n    component: HandleField,\n    displayName: _t(\"Handle\"),\n    supportedTypes: [\"integer\"],\n    isEmpty: () => false,\n    listViewWidth: 20,\n};\n\nregistry.category(\"fields\").add(\"handle\", handleField);\n", "import { registry } from \"@web/core/registry\";\nimport { TextField, textField } from \"../text/text_field\";\n\nexport class HtmlField extends TextField {\n    static template = \"web.HtmlField\";\n}\n\nexport const htmlField = {\n    ...textField,\n    component: HtmlField,\n};\n\nregistry.category(\"fields\").add(\"html\", htmlField);\n", "import { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { Component, useEffect, useRef } from \"@odoo/owl\";\n\nexport class IframeWrapperField extends Component {\n    static template = \"web.IframeWrapperField\";\n    static props = {\n        ...standardFieldProps,\n    };\n\n    setup() {\n        this.iframeRef = useRef(\"iframe\");\n\n        useEffect(\n            (value) => {\n                /**\n                 * The document.write is not recommended. It is better to manipulate the DOM through $.appendChild and\n                 * others. In our case though, we deal with an iframe without src attribute and with metadata to put in\n                 * head tag. If we use the usual dom methods, the iframe is automatically created with its document\n                 * component containing html > head & body. Therefore, if we want to make it work that way, we would\n                 * need to receive each piece at a time to  append it to this document (with this.record.data and extra\n                 * model fields or with an rpc). It also cause other difficulties getting attribute on the most parent\n                 * nodes, parsing to HTML complex elements, etc.\n                 * Therefore, document.write makes it much more trivial in our situation.\n                 */\n                const iframeDoc = this.iframeRef.el.contentDocument;\n                iframeDoc.open();\n                iframeDoc.write(value);\n                iframeDoc.close();\n            },\n            () => [this.props.record.data[this.props.name]]\n        );\n    }\n}\n\nexport const iframeWrapperField = {\n    component: IframeWrapperField,\n    displayName: _t(\"Wrap raw html within an iframe\"),\n    // If HTML, don't forget to adjust the sanitize options to avoid stripping most of the metadata\n    supportedTypes: [\"text\", \"html\"],\n};\n\nregistry.category(\"fields\").add(\"iframe_wrapper\", iframeWrapperField);\n", "import { isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { imageUrl } from \"@web/core/utils/urls\";\nimport { isBinarySize } from \"@web/core/utils/binary\";\nimport { FileUploader } from \"../file_handler\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component, useState, onWillRender } from \"@odoo/owl\";\nconst { DateTime } = luxon;\n\nexport const fileTypeMagicWordMap = {\n    \"/\": \"jpg\",\n    R: \"gif\",\n    i: \"png\",\n    P: \"svg+xml\",\n    U: \"webp\",\n};\nconst placeholder = \"/web/static/img/placeholder.png\";\n\nexport class ImageField extends Component {\n    static template = \"web.ImageField\";\n    static components = {\n        FileUploader,\n    };\n    static props = {\n        ...standardFieldProps,\n        alt: { type: String, optional: true },\n        enableZoom: { type: Boolean, optional: true },\n        imgClass: { type: String, optional: true },\n        zoomDelay: { type: Number, optional: true },\n        previewImage: { type: String, optional: true },\n        acceptedFileExtensions: { type: String, optional: true },\n        width: { type: Number, optional: true },\n        height: { type: Number, optional: true },\n        reload: { type: Boolean, optional: true },\n        convertToWebp: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        acceptedFileExtensions: \"image/*\",\n        alt: _t(\"Binary file\"),\n        imgClass: \"\",\n        reload: true,\n    };\n\n    setup() {\n        this.notification = useService(\"notification\");\n        this.orm = useService(\"orm\");\n        this.isMobile = isMobileOS();\n        this.state = useState({\n            isValid: true,\n        });\n        this.lastURL = undefined;\n\n        if (this.fieldType === \"many2one\" && !this.props.previewImage) {\n            throw new Error(\n                \"ImageField: previewImage must be provided when set on a many2one field\"\n            );\n        }\n        const field = this.props.record.fields[this.props.name];\n        if (field.related?.includes(\".\")) {\n            this.lastUpdate = DateTime.now();\n            let key = this.props.value;\n            onWillRender(() => {\n                const nextKey = this.props.value;\n\n                if (key !== nextKey) {\n                    this.lastUpdate = DateTime.now();\n                }\n\n                key = nextKey;\n            });\n        }\n    }\n\n    get imgAlt() {\n        if (this.fieldType === \"many2one\" && this.props.record.data[this.props.name]) {\n            return this.props.record.data[this.props.name][1];\n        }\n        return this.props.alt;\n    }\n\n    get imgClass() {\n        return [\"img\", \"img-fluid\"].concat(this.props.imgClass.split(\" \")).join(\" \");\n    }\n\n    get fieldType() {\n        return this.props.record.fields[this.props.name].type;\n    }\n\n    get rawCacheKey() {\n        return this.lastUpdate || this.props.record.data.write_date;\n    }\n\n    get sizeStyle() {\n        let style = \"\";\n        if (this.props.width) {\n            style += `max-width: ${this.props.width}px;`;\n            if (!this.props.height) {\n                style += `height: auto; max-height: 100%;`;\n            }\n        }\n        if (this.props.height) {\n            style += `max-height: ${this.props.height}px;`;\n            if (!this.props.width) {\n                style += `width: auto; max-width: 100%;`;\n            }\n        }\n        return style;\n    }\n    get hasTooltip() {\n        return this.props.enableZoom && this.props.record.data[this.props.name];\n    }\n    get tooltipAttributes() {\n        const fieldName = this.fieldType === \"many2one\" ? this.props.previewImage : this.props.name;\n        return {\n            template: \"web.ImageZoomTooltip\",\n            info: JSON.stringify({ url: this.getUrl(fieldName) }),\n        };\n    }\n\n    getUrl(imageFieldName) {\n        if (!this.props.reload && this.lastURL) {\n            return this.lastURL;\n        }\n        if (!this.props.record.data[this.props.name] || !this.state.isValid) {\n            return placeholder;\n        }\n        if (this.fieldType === \"many2one\") {\n            this.lastURL = imageUrl(\n                this.props.record.fields[this.props.name].relation,\n                this.props.record.data[this.props.name][0],\n                imageFieldName,\n                { unique: this.rawCacheKey }\n            );\n        } else if (isBinarySize(this.props.record.data[this.props.name])) {\n            this.lastURL = imageUrl(\n                this.props.record.resModel,\n                this.props.record.resId,\n                imageFieldName,\n                { unique: this.rawCacheKey }\n            );\n        } else {\n            // Use magic-word technique for detecting image type\n            const magic = fileTypeMagicWordMap[this.props.record.data[this.props.name][0]] || \"png\";\n            this.lastURL = `data:image/${magic};base64,${this.props.record.data[this.props.name]}`;\n        }\n        return this.lastURL;\n    }\n    onFileRemove() {\n        this.state.isValid = true;\n        this.props.record.update({ [this.props.name]: false });\n    }\n    async onFileUploaded(info) {\n        this.state.isValid = true;\n        if (\n            this.props.convertToWebp &&\n            ![\"image/gif\", \"image/svg+xml\", \"image/webp\"].includes(info.type)\n        ) {\n            const image = document.createElement(\"img\");\n            image.src = `data:${info.type};base64,${info.data}`;\n            await new Promise((resolve) => 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.drawImage(image, 0, 0);\n\n            info.data = canvas.toDataURL(\"image/webp\", 0.75).split(\",\")[1];\n            info.type = \"image/webp\";\n            info.name = info.name.replace(/\\.[^/.]+$/, \".webp\");\n        }\n        if (info.type === \"image/webp\") {\n            // Generate alternate sizes and format for reports.\n            const image = document.createElement(\"img\");\n            image.src = `data:image/webp;base64,${info.data}`;\n            await new Promise((resolve) => image.addEventListener(\"load\", resolve));\n            const originalSize = Math.max(image.width, image.height);\n            const smallerSizes = [1024, 512, 256, 128].filter((size) => size < originalSize);\n            let referenceId = undefined;\n            for (const size of [originalSize, ...smallerSizes]) {\n                const ratio = size / originalSize;\n                const canvas = document.createElement(\"canvas\");\n                canvas.width = image.width * ratio;\n                canvas.height = image.height * ratio;\n                const ctx = canvas.getContext(\"2d\");\n                ctx.fillStyle = \"transparent\";\n                ctx.fillRect(0, 0, canvas.width, canvas.height);\n                ctx.drawImage(\n                    image,\n                    0,\n                    0,\n                    image.width,\n                    image.height,\n                    0,\n                    0,\n                    canvas.width,\n                    canvas.height\n                );\n                const [resizedId] = await this.orm.call(\"ir.attachment\", \"create_unique\", [\n                    [\n                        {\n                            name: info.name,\n                            description: size === originalSize ? \"\" : `resize: ${size}`,\n                            datas:\n                                size === originalSize\n                                    ? info.data\n                                    : canvas.toDataURL(\"image/webp\", 0.75).split(\",\")[1],\n                            res_id: referenceId,\n                            res_model: \"ir.attachment\",\n                            mimetype: \"image/webp\",\n                        },\n                    ],\n                ]);\n                referenceId = referenceId || resizedId; // Keep track of original.\n                // Converted to JPEG for use in PDF files, alpha values will default to white\n                await this.orm.call(\"ir.attachment\", \"create_unique\", [\n                    [\n                        {\n                            name: info.name.replace(/\\.webp$/, \".jpg\"),\n                            description: \"format: jpeg\",\n                            datas: canvas.toDataURL(\"image/jpeg\", 0.75).split(\",\")[1],\n                            res_id: resizedId,\n                            res_model: \"ir.attachment\",\n                            mimetype: \"image/jpeg\",\n                        },\n                    ],\n                ]);\n            }\n        }\n        this.props.record.update({ [this.props.name]: info.data });\n    }\n    onLoadFailed() {\n        this.state.isValid = false;\n    }\n}\n\nexport const imageField = {\n    component: ImageField,\n    displayName: _t(\"Image\"),\n    supportedAttributes: [\n        {\n            label: _t(\"Alternative text\"),\n            name: \"alt\",\n            type: \"string\",\n        },\n    ],\n    supportedOptions: [\n        {\n            label: _t(\"Reload\"),\n            name: \"reload\",\n            type: \"boolean\",\n            default: true,\n        },\n        {\n            label: _t(\"Enable zoom\"),\n            name: \"zoom\",\n            type: \"boolean\",\n        },\n        {\n            label: _t(\"Convert to webp\"),\n            name: \"convert_to_webp\",\n            type: \"boolean\",\n        },\n        {\n            label: _t(\"Zoom delay\"),\n            name: \"zoom_delay\",\n            type: \"number\",\n            help: _t(\"Delay the apparition of the zoomed image with a value in milliseconds\"),\n        },\n        {\n            label: _t(\"Accepted file extensions\"),\n            name: \"accepted_file_extensions\",\n            type: \"string\",\n        },\n        {\n            label: _t(\"Size\"),\n            name: \"size\",\n            type: \"selection\",\n            choices: [\n                { label: _t(\"Small\"), value: \"[0,90]\" },\n                { label: _t(\"Medium\"), value: \"[0,180]\" },\n                { label: _t(\"Large\"), value: \"[0,270]\" },\n            ],\n        },\n        {\n            label: _t(\"Preview image\"),\n            name: \"preview_image\",\n            type: \"field\",\n            availableTypes: [\"binary\"],\n        },\n    ],\n    supportedTypes: [\"binary\", \"many2one\"],\n    fieldDependencies: [{ name: \"write_date\", type: \"datetime\" }],\n    isEmpty: () => false,\n    extractProps: ({ attrs, options }) => ({\n        alt: attrs.alt,\n        enableZoom: options.zoom,\n        convertToWebp: options.convert_to_webp,\n        imgClass: options.img_class,\n        zoomDelay: options.zoom_delay,\n        previewImage: options.preview_image,\n        acceptedFileExtensions: options.accepted_file_extensions,\n        width: options.size && Boolean(options.size[0]) ? options.size[0] : attrs.width,\n        height: options.size && Boolean(options.size[1]) ? options.size[1] : attrs.height,\n        reload: \"reload\" in options ? Boolean(options.reload) : true,\n    }),\n};\n\nregistry.category(\"fields\").add(\"image\", imageField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component, useState } from \"@odoo/owl\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\n\nexport class ImageUrlField extends Component {\n    static template = \"web.ImageUrlField\";\n    static props = {\n        ...standardFieldProps,\n        width: { type: Number, optional: true },\n        height: { type: Number, optional: true },\n    };\n\n    static fallbackSrc = \"/web/static/img/placeholder.png\";\n\n    setup() {\n        this.notification = useService(\"notification\");\n        this.state = useState({\n            src: this.props.record.data[this.props.name],\n        });\n\n        useRecordObserver((record) => {\n            this.state.src = record.data[this.props.name];\n        });\n    }\n\n    get sizeStyle() {\n        let style = \"\";\n        if (this.props.width) {\n            style += `max-width: ${this.props.width}px;`;\n        }\n        if (this.props.height) {\n            style += `max-height: ${this.props.height}px;`;\n        }\n        return style;\n    }\n\n    onLoadFailed() {\n        this.state.src = this.constructor.fallbackSrc;\n    }\n}\n\nexport const imageUrlField = {\n    component: ImageUrlField,\n    displayName: _t(\"Image\"),\n    supportedOptions: [\n        {\n            label: _t(\"Size\"),\n            name: \"size\",\n            type: \"selection\",\n            choices: [\n                { label: _t(\"Small\"), value: \"[0,90]\" },\n                { label: _t(\"Medium\"), value: \"[0,180]\" },\n                { label: _t(\"Large\"), value: \"[0,270]\" },\n            ],\n        },\n    ],\n    supportedTypes: [\"char\"],\n    extractProps: ({ attrs, options }) => ({\n        width: options.size ? options.size[0] : attrs.width,\n        height: options.size ? options.size[1] : attrs.height,\n    }),\n};\n\nregistry.category(\"fields\").add(\"image_url\", imageUrlField);\n", "import { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { useBus } from \"@web/core/utils/hooks\";\n\nimport { useComponent, useEffect, useRef } from \"@odoo/owl\";\n\n/**\n * This hook is meant to be used by field components that use an input or\n * textarea to edit their value. Its purpose is to prevent that value from being\n * erased by an update of the model (typically coming from an onchange) when the\n * user is currently editing it.\n *\n * @param {() => string} getValue a function that returns the value to write in\n *   the input, if the user isn't currently editing it\n * @param {string} [refName=\"input\"] the ref of the input/textarea\n * @param {boolean} preventLineBreaks Prevent line breaks in input when set\n */\nexport function useInputField(params) {\n    const inputRef = params.ref || useRef(params.refName || \"input\");\n    const component = useComponent();\n\n    /*\n     * A field is dirty if it is no longer sync with the model\n     * More specifically, a field is no longer dirty after it has *tried* to update the value in the model.\n     * An invalid value will thefore not be dirty even if the model will not actually store the invalid value.\n     */\n    let isDirty = false;\n\n    /**\n     * The last value that has been commited to the model.\n     * Not changed in case of invalid field value.\n     */\n    let lastSetValue = null;\n\n    /**\n     * Track the fact that there is a change sent to the model that hasn't been acknowledged yet\n     * (e.g. because the onchange is still pending). This is necessary if we must do an urgent save,\n     * as we have to re-send that change for the write that will be done directly.\n     * FIXME: this could/should be handled by the model itself, when it will be rewritten\n     */\n    let pendingUpdate = false;\n\n    /**\n     * When a user types, we need to set the field as dirty.\n     */\n    function onInput(ev) {\n        isDirty = ev.target.value !== lastSetValue;\n        if (params.preventLineBreaks && ev.inputType === \"insertFromPaste\") {\n            ev.target.value = ev.target.value.replace(/[\\r\\n]+/g, \" \");\n        }\n        component.props.record.model.bus.trigger(\"FIELD_IS_DIRTY\", isDirty);\n        if (!component.props.record.isValid) {\n            component.props.record.resetFieldValidity(component.props.name);\n        }\n    }\n\n    /**\n     * On blur, we consider the field no longer dirty, even if it were to be invalid.\n     * However, if the field is invalid, the new value will not be committed to the model.\n     */\n    async function onChange(ev) {\n        if (isDirty) {\n            isDirty = false;\n            let isInvalid = false;\n            let val = ev.target.value;\n            if (params.parse) {\n                try {\n                    val = params.parse(val);\n                } catch {\n                    component.props.record.setInvalidField(component.props.name);\n                    isInvalid = true;\n                }\n            }\n\n            if (!isInvalid) {\n                if (val !== component.props.record.data[component.props.name]) {\n                    lastSetValue = inputRef.el.value;\n                    pendingUpdate = true;\n                    await component.props.record.update({ [component.props.name]: val });\n                    pendingUpdate = false;\n                    component.props.record.model.bus.trigger(\"FIELD_IS_DIRTY\", isDirty);\n                } else {\n                    inputRef.el.value = params.getValue();\n                }\n            }\n        }\n    }\n    function onKeydown(ev) {\n        const hotkey = getActiveHotkey(ev);\n        if ([\"enter\", \"tab\", \"shift+tab\"].includes(hotkey)) {\n            commitChanges(false);\n        }\n        if (params.preventLineBreaks && [\"enter\", \"shift+enter\"].includes(hotkey)) {\n            ev.preventDefault();\n        }\n    }\n\n    useEffect(\n        (inputEl) => {\n            if (inputEl) {\n                inputEl.addEventListener(\"input\", onInput);\n                inputEl.addEventListener(\"change\", onChange);\n                inputEl.addEventListener(\"keydown\", onKeydown);\n                return () => {\n                    inputEl.removeEventListener(\"input\", onInput);\n                    inputEl.removeEventListener(\"change\", onChange);\n                    inputEl.removeEventListener(\"keydown\", onKeydown);\n                };\n            }\n        },\n        () => [inputRef.el]\n    );\n\n    /**\n     * Sometimes, a patch can happen with possible a new value for the field\n     * If the user was typing a new value (isDirty) or the field is still invalid,\n     * we need to do nothing.\n     * If it is not such a case, we update the field with the new value.\n     */\n    useEffect(() => {\n        // We need to call getValue before the condition to always observe\n        // the corresponding value in the record. Otherwise, in some cases,\n        // if the value in the record change the useEffect isn't triggered.\n        const value = params.getValue();\n        if (\n            inputRef.el &&\n            !isDirty &&\n            !component.props.record.isFieldInvalid(component.props.name)\n        ) {\n            inputRef.el.value = value;\n            lastSetValue = inputRef.el.value;\n        }\n    });\n\n    const { model } = component.props.record;\n    useBus(model.bus, \"WILL_SAVE_URGENTLY\", () => commitChanges(true));\n    useBus(model.bus, \"NEED_LOCAL_CHANGES\", (ev) => ev.detail.proms.push(commitChanges()));\n\n    /**\n     * Roughly the same as onChange, but called at more specific / critical times. (See bus events)\n     */\n    async function commitChanges(urgent) {\n        if (!inputRef.el) {\n            return;\n        }\n\n        isDirty = inputRef.el.value !== lastSetValue;\n        if (isDirty || (urgent && pendingUpdate)) {\n            let isInvalid = false;\n            isDirty = false;\n            let val = inputRef.el.value;\n            if (params.parse) {\n                try {\n                    val = params.parse(val);\n                } catch {\n                    isInvalid = true;\n                    if (urgent) {\n                        return;\n                    } else {\n                        component.props.record.setInvalidField(component.props.name);\n                    }\n                }\n            }\n\n            if (isInvalid) {\n                return;\n            }\n\n            if ((val || false) !== (component.props.record.data[component.props.name] || false)) {\n                lastSetValue = inputRef.el.value;\n                await component.props.record.update({ [component.props.name]: val });\n                component.props.record.model.bus.trigger(\"FIELD_IS_DIRTY\", false);\n            } else {\n                inputRef.el.value = params.getValue();\n            }\n        }\n    }\n\n    return inputRef;\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { formatInteger } from \"../formatters\";\nimport { parseInteger } from \"../parsers\";\nimport { useInputField } from \"../input_field_hook\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { useNumpadDecimal } from \"../numpad_decimal_hook\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class IntegerField extends Component {\n    static template = \"web.IntegerField\";\n    static props = {\n        ...standardFieldProps,\n        formatNumber: { type: Boolean, optional: true },\n        humanReadable: { type: Boolean, optional: true },\n        decimals: { type: Number, optional: true },\n        inputType: { type: String, optional: true },\n        step: { type: Number, optional: true },\n        placeholder: { type: String, optional: true },\n    };\n    static defaultProps = {\n        formatNumber: true,\n        humanReadable: false,\n        inputType: \"text\",\n        decimals: 0,\n    };\n\n    setup() {\n        this.state = useState({\n            hasFocus: false,\n        });\n        useInputField({\n            getValue: () => this.formattedValue,\n            refName: \"numpadDecimal\",\n            parse: (v) => parseInteger(v),\n        });\n        useNumpadDecimal();\n    }\n\n    onFocusIn() {\n        this.state.hasFocus = true;\n    }\n\n    onFocusOut() {\n        this.state.hasFocus = false;\n    }\n\n    get formattedValue() {\n        if (\n            !this.props.formatNumber ||\n            (!this.props.readonly && this.props.inputType === \"number\")\n        ) {\n            return this.value;\n        }\n        if (this.props.humanReadable && !this.state.hasFocus) {\n            return formatInteger(this.value, {\n                humanReadable: true,\n                decimals: this.props.decimals,\n            });\n        } else {\n            return formatInteger(this.value, { humanReadable: false });\n        }\n    }\n\n    get value() {\n        return this.props.record.data[this.props.name];\n    }\n}\n\nexport const integerField = {\n    component: IntegerField,\n    displayName: _t(\"Integer\"),\n    supportedOptions: [\n        {\n            label: _t(\"Format number\"),\n            name: \"enable_formatting\",\n            type: \"boolean\",\n            help: _t(\n                \"Format the value\u00a0according to your language setup - e.g. thousand separators, rounding, etc.\"\n            ),\n            default: true,\n        },\n        {\n            label: _t(\"Type\"),\n            name: \"type\",\n            type: \"string\",\n        },\n        {\n            label: _t(\"Step\"),\n            name: \"step\",\n            type: \"number\",\n        },\n        {\n            label: _t(\"User-friendly format\"),\n            name: \"human_readable\",\n            type: \"boolean\",\n            help: _t(\"Use a human readable format (e.g.: 500G instead of 500,000,000,000).\"),\n        },\n        {\n            label: _t(\"Decimals\"),\n            name: \"decimals\",\n            type: \"number\",\n            default: 0,\n            help: _t(\"Use it with the 'User-friendly format' option to customize the formatting.\"),\n        },\n    ],\n    supportedTypes: [\"integer\"],\n    isEmpty: (record, fieldName) => record.data[fieldName] === false,\n    extractProps: ({ attrs, options }) => ({\n        formatNumber:\n            options?.enable_formatting !== undefined ? Boolean(options.enable_formatting) : true,\n        humanReadable: !!options.human_readable,\n        inputType: options.type,\n        step: options.step,\n        placeholder: attrs.placeholder,\n        decimals: options.decimals || 0,\n    }),\n};\n\nregistry.category(\"fields\").add(\"integer\", integerField);\n", "import { loadBundle } from \"@web/core/assets\";\nimport { registry } from \"@web/core/registry\";\nimport { getColor, hexToRGBA, getCustomColor } from \"@web/core/colors/colors\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component, onWillStart, useEffect, useRef } from \"@odoo/owl\";\nimport { cookie } from \"@web/core/browser/cookie\";\n\nconst colorScheme = cookie.get(\"color_scheme\");\nconst GRAPH_GRID_COLOR = getCustomColor(colorScheme, \"#d8dadd\", \"#3C3E4B\");\nconst GRAPH_LABEL_COLOR = getCustomColor(colorScheme, \"#111827\", \"#E4E4E4\");\nexport class JournalDashboardGraphField extends Component {\n    static template = \"web.JournalDashboardGraphField\";\n    static props = {\n        ...standardFieldProps,\n        graphType: String,\n    };\n\n    setup() {\n        this.chart = null;\n        this.canvasRef = useRef(\"canvas\");\n        this.data = JSON.parse(this.props.record.data[this.props.name]);\n\n        onWillStart(async () => await loadBundle(\"web.chartjs_lib\"));\n\n        useEffect(() => {\n            this.renderChart();\n            return () => {\n                if (this.chart) {\n                    this.chart.destroy();\n                }\n            };\n        });\n    }\n\n    /**\n     * Instantiates a Chart (Chart.js lib) to render the graph according to\n     * the current config.\n     */\n    renderChart() {\n        if (this.chart) {\n            this.chart.destroy();\n        }\n        let config;\n        if (this.props.graphType === \"line\") {\n            config = this.getLineChartConfig();\n        } else if (this.props.graphType === \"bar\") {\n            config = this.getBarChartConfig();\n        }\n        this.chart = new Chart(this.canvasRef.el, config);\n    }\n    getLineChartConfig() {\n        const labels = this.data[0].values.map(function (pt) {\n            return pt.x;\n        });\n        const color10 = getColor(3, cookie.get(\"color_scheme\"), \"odoo\");\n        const borderColor = this.data[0].is_sample_data ? hexToRGBA(color10, 0.1) : color10;\n        const backgroundColor = this.data[0].is_sample_data\n            ? hexToRGBA(color10, 0.05)\n            : hexToRGBA(color10, 0.2);\n        return {\n            type: \"line\",\n            data: {\n                labels,\n                datasets: [\n                    {\n                        backgroundColor,\n                        borderColor,\n                        data: this.data[0].values,\n                        fill: \"start\",\n                        label: this.data[0].key,\n                        borderWidth: 2,\n                    },\n                ],\n            },\n            options: {\n                plugins: {\n                    legend: { display: false },\n                    tooltip: {\n                        enabled: !this.data[0].is_sample_data,\n                        intersect: false,\n                        position: \"nearest\",\n                        caretSize: 0,\n                    },\n                },\n                scales: {\n                    y: {\n                        display: false,\n                    },\n                    x: {\n                        display: false,\n                    },\n                },\n                maintainAspectRatio: false,\n                elements: {\n                    line: {\n                        tension: 0.000001,\n                    },\n                },\n            },\n        };\n    }\n\n    getBarChartConfig() {\n        const data = [];\n        const labels = [];\n        const backgroundColor = [];\n\n        const color13 = getColor(2, cookie.get(\"color_scheme\"), \"odoo\");\n        const color19 = getColor(1, cookie.get(\"color_scheme\"), \"odoo\");\n        this.data[0].values.forEach((pt) => {\n            data.push(pt.value);\n            labels.push(pt.label);\n            if (pt.type === \"past\") {\n                backgroundColor.push(color13);\n            } else if (pt.type === \"future\") {\n                backgroundColor.push(color19);\n            } else {\n                backgroundColor.push(getCustomColor(colorScheme, \"#ebebeb\", \"#3C3E4B\"));\n            }\n        });\n        return {\n            type: \"bar\",\n            data: {\n                labels,\n                datasets: [\n                    {\n                        backgroundColor,\n                        data,\n                        fill: \"start\",\n                        label: this.data[0].key,\n                    },\n                ],\n            },\n            options: {\n                plugins: {\n                    legend: { display: false },\n                    tooltip: {\n                        enabled: !this.data[0].is_sample_data,\n                        intersect: false,\n                        position: \"nearest\",\n                        caretSize: 0,\n                    },\n                },\n                scales: {\n                    y: {\n                        display: false,\n                    },\n                    x: {\n                        grid: {\n                            color: GRAPH_GRID_COLOR,\n                        },\n                        ticks: {\n                            color: GRAPH_LABEL_COLOR,\n                        },\n                        border: {\n                            color: GRAPH_GRID_COLOR,\n                        },\n                    },\n                },\n                maintainAspectRatio: false,\n                elements: {\n                    line: {\n                        tension: 0.000001,\n                    },\n                },\n            },\n        };\n    }\n}\n\nexport const journalDashboardGraphField = {\n    component: JournalDashboardGraphField,\n    supportedTypes: [\"text\"],\n    extractProps: ({ attrs }) => ({\n        graphType: attrs.graph_type,\n    }),\n};\n\nregistry.category(\"fields\").add(\"dashboard_graph\", journalDashboardGraphField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class JsonField extends Component {\n    static template = \"web.JsonbField\";\n    static props = {\n        ...standardFieldProps,\n    };\n    get formattedValue() {\n        const value = this.props.record.data[this.props.name];\n        return value ? JSON.stringify(value) : \"\";\n    }\n}\n\nexport const jsonField = {\n    component: JsonField,\n    displayName: _t(\"Json\"),\n    supportedTypes: [\"jsonb\"],\n};\n\nregistry.category(\"fields\").add(\"jsonb\", jsonField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { ColorList } from \"@web/core/colorlist/colorlist\";\nimport { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component } from \"@odoo/owl\";\n\nclass KanbanColorPickerField extends Component {\n    static template = \"web.KanbanColorPickerField\";\n    static props = standardFieldProps;\n\n    get colors() {\n        return ColorList.COLORS;\n    }\n\n    selectColor(colorIndex) {\n        return this.props.record.update({ [this.props.name]: colorIndex }, { save: true });\n    }\n}\n\nexport const kanbanColorPickerField = {\n    component: KanbanColorPickerField,\n    displayName: _t(\"Color Picker\"),\n    extractProps(fieldInfo, dynamicInfo) {\n        return {\n            readonly: dynamicInfo.readonly,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"kanban_color_picker\", kanbanColorPickerField);\n", "import { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { formatSelection } from \"../formatters\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class LabelSelectionField extends Component {\n    static template = \"web.LabelSelectionField\";\n    static props = {\n        ...standardFieldProps,\n        classesObj: { type: Object, optional: true },\n    };\n    static defaultProps = {\n        classesObj: {},\n    };\n\n    get className() {\n        return this.props.classesObj[this.props.record.data[this.props.name]] || \"primary\";\n    }\n    get string() {\n        return formatSelection(this.props.record.data[this.props.name], {\n            selection: Array.from(this.props.record.fields[this.props.name].selection),\n        });\n    }\n}\n\nexport const labelSelectionField = {\n    component: LabelSelectionField,\n    displayName: _t(\"Label Selection\"),\n    supportedOptions: [\n        {\n            label: _t(\"Classes\"),\n            name: \"classes\",\n            type: \"string\",\n        },\n    ],\n    supportedTypes: [\"selection\"],\n    extractProps: ({ options }) => ({\n        classesObj: options.classes,\n    }),\n};\n\nregistry.category(\"fields\").add(\"label_selection\", labelSelectionField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { FileInput } from \"@web/core/file_input/file_input\";\nimport { useX2ManyCrud } from \"@web/views/fields/relational_utils\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class Many2ManyBinaryField extends Component {\n    static template = \"web.Many2ManyBinaryField\";\n    static components = {\n        FileInput,\n    };\n    static props = {\n        ...standardFieldProps,\n        acceptedFileExtensions: { type: String, optional: true },\n        className: { type: String, optional: true },\n        numberOfFiles: { type: Number, optional: true },\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.notification = useService(\"notification\");\n        this.operations = useX2ManyCrud(() => this.props.record.data[this.props.name], true);\n    }\n\n    get uploadText() {\n        return this.props.record.fields[this.props.name].string;\n    }\n    get files() {\n        return this.props.record.data[this.props.name].records.map((record) => {\n            return {\n                ...record.data,\n                id: record.resId,\n            };\n        });\n    }\n\n    getUrl(id) {\n        return \"/web/content/\" + id + \"?download=true\";\n    }\n\n    getExtension(file) {\n        return file.name.replace(/^.*\\./, \"\");\n    }\n\n    async onFileUploaded(files) {\n        for (const file of files) {\n            if (file.error) {\n                return this.notification.add(file.error, {\n                    title: _t(\"Uploading error\"),\n                    type: \"danger\",\n                });\n            }\n            await this.operations.saveRecord([file.id]);\n        }\n    }\n\n    async onFileRemove(deleteId) {\n        const record = this.props.record.data[this.props.name].records.find(\n            (record) => record.resId === deleteId\n        );\n        this.operations.removeRecord(record);\n    }\n}\n\nexport const many2ManyBinaryField = {\n    component: Many2ManyBinaryField,\n    supportedOptions: [\n        {\n            label: _t(\"Accepted file extensions\"),\n            name: \"accepted_file_extensions\",\n            type: \"string\",\n        },\n        {\n            label: _t(\"Number of files\"),\n            name: \"number_of_files\",\n            type: \"integer\",\n        },\n    ],\n    supportedTypes: [\"many2many\"],\n    isEmpty: () => false,\n    relatedFields: [\n        { name: \"name\", type: \"char\" },\n        { name: \"mimetype\", type: \"char\" },\n    ],\n    extractProps: ({ attrs, options }) => ({\n        acceptedFileExtensions: options.accepted_file_extensions,\n        className: attrs.class,\n        numberOfFiles: options.number_of_files,\n    }),\n};\n\nregistry.category(\"fields\").add(\"many2many_binary\", many2ManyBinaryField);\n", "import { Component, onWillUnmount } from \"@odoo/owl\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useBus } from \"@web/core/utils/hooks\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { getFieldDomain } from \"@web/model/relational_model/utils\";\nimport { useSpecialData } from \"@web/views/fields/relational_utils\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nexport class Many2ManyCheckboxesField extends Component {\n    static template = \"web.Many2ManyCheckboxesField\";\n    static components = { CheckBox };\n    static props = {\n        ...standardFieldProps,\n        domain: { type: [Array, Function], optional: true },\n    };\n\n    setup() {\n        this.specialData = useSpecialData((orm, props) => {\n            const { relation } = props.record.fields[props.name];\n            const domain = getFieldDomain(props.record, props.name, props.domain);\n            return orm.call(relation, \"name_search\", [\"\", domain]);\n        });\n        // these two sets track pending changes in the relation, and allow us to\n        // batch consecutive changes into a single replaceWith, thus saving\n        // unnecessary potential intermediate onchanges\n        this.idsToAdd = new Set();\n        this.idsToRemove = new Set();\n        this.debouncedCommitChanges = debounce(this.commitChanges.bind(this), 500);\n        useBus(this.props.record.model.bus, \"NEED_LOCAL_CHANGES\", this.commitChanges.bind(this));\n        onWillUnmount(this.commitChanges.bind(this));\n    }\n\n    get items() {\n        return this.specialData.data;\n    }\n\n    isSelected(item) {\n        return this.props.record.data[this.props.name].currentIds.includes(item[0]);\n    }\n\n    commitChanges() {\n        if (this.idsToAdd.size === 0 && this.idsToRemove.size === 0) {\n            return;\n        }\n        const result = this.props.record.data[this.props.name].addAndRemove({\n            add: [...this.idsToAdd],\n            remove: [...this.idsToRemove],\n        });\n        this.idsToAdd.clear();\n        this.idsToRemove.clear();\n        return result;\n    }\n\n    onChange(resId, checked) {\n        if (checked) {\n            if (this.idsToRemove.has(resId)) {\n                this.idsToRemove.delete(resId);\n            } else {\n                this.idsToAdd.add(resId);\n            }\n        } else {\n            if (this.idsToAdd.has(resId)) {\n                this.idsToAdd.delete(resId);\n            } else {\n                this.idsToRemove.add(resId);\n            }\n        }\n        this.debouncedCommitChanges();\n    }\n}\n\nexport const many2ManyCheckboxesField = {\n    component: Many2ManyCheckboxesField,\n    displayName: _t(\"Checkboxes\"),\n    supportedTypes: [\"many2many\"],\n    isEmpty: () => false,\n    extractProps(fieldInfo, dynamicInfo) {\n        return {\n            domain: dynamicInfo.domain,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"many2many_checkboxes\", many2ManyCheckboxesField);\n", "import { registry } from \"@web/core/registry\";\nimport { Many2ManyTagsField, many2ManyTagsField } from \"./many2many_tags_field\";\n\nexport class KanbanMany2ManyTagsField extends Many2ManyTagsField {\n    static template = \"web.KanbanMany2ManyTagsField\";\n\n    get tags() {\n        return super.tags.reduce((kanbanTags, tag) => {\n            if (tag.colorIndex !== 0) {\n                delete tag.onClick;\n                kanbanTags.push(tag);\n            }\n            return kanbanTags;\n        }, []);\n    }\n}\n\nexport const kanbanMany2ManyTagsField = {\n    ...many2ManyTagsField,\n    component: KanbanMany2ManyTagsField,\n};\n\nregistry.category(\"fields\").add(\"kanban.many2many_tags\", kanbanMany2ManyTagsField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { ColorList } from \"@web/core/colorlist/colorlist\";\nimport { Domain } from \"@web/core/domain\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport {\n    Many2XAutocomplete,\n    useActiveActions,\n    useX2ManyCrud,\n    useOpenMany2XRecord,\n} from \"@web/views/fields/relational_utils\";\nimport { registry } from \"@web/core/registry\";\nimport { Mutex } from \"@web/core/utils/concurrency\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { TagsList } from \"@web/core/tags_list/tags_list\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useTagNavigation } from \"@web/core/record_selectors/tag_navigation_hook\";\n\nimport { Component, useRef } from \"@odoo/owl\";\nimport { getFieldDomain } from \"@web/model/relational_model/utils\";\n\nclass Many2ManyTagsFieldColorListPopover extends Component {\n    static template = \"web.Many2ManyTagsFieldColorListPopover\";\n    static components = {\n        CheckBox,\n        ColorList,\n    };\n    static props = {\n        colors: Array,\n        tag: Object,\n        switchTagColor: Function,\n        onTagVisibilityChange: Function,\n        close: Function,\n    };\n}\n\nexport class Many2ManyTagsField extends Component {\n    static template = \"web.Many2ManyTagsField\";\n    static components = {\n        TagsList,\n        Many2XAutocomplete,\n    };\n    static props = {\n        ...standardFieldProps,\n        canCreate: { type: Boolean, optional: true },\n        canQuickCreate: { type: Boolean, optional: true },\n        canCreateEdit: { type: Boolean, optional: true },\n        colorField: { type: String, optional: true },\n        createDomain: { type: [Array, Boolean], optional: true },\n        domain: { type: [Array, Function], optional: true },\n        context: { type: Object, optional: true },\n        placeholder: { type: String, optional: true },\n        nameCreateField: { type: String, optional: true },\n        string: { type: String, optional: true },\n        noSearchMore: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        canCreate: true,\n        canQuickCreate: true,\n        canCreateEdit: true,\n        nameCreateField: \"name\",\n        context: {},\n    };\n\n    static RECORD_COLORS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];\n    static SEARCH_MORE_LIMIT = 320;\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.previousColorsMap = {};\n        this.popover = usePopover(this.constructor.components.Popover);\n        this.dialog = useService(\"dialog\");\n        this.dialogClose = [];\n        this.onTagKeydown = useTagNavigation(\n            \"many2ManyTagsField\",\n            this.deleteTagByIndex.bind(this)\n        );\n        this.autoCompleteRef = useRef(\"autoComplete\");\n        this.mutex = new Mutex();\n\n        const { saveRecord, removeRecord } = useX2ManyCrud(\n            () => this.props.record.data[this.props.name],\n            true\n        );\n\n        this.activeActions = useActiveActions({\n            fieldType: \"many2many\",\n            crudOptions: {\n                create: this.props.canCreate && this.props.createDomain,\n                createEdit: this.props.canCreateEdit,\n                onDelete: removeRecord,\n                edit: this.props.record.isInEdition,\n            },\n            getEvalParams: (props) => {\n                return {\n                    evalContext: this.evalContext,\n                    readonly: props.readonly,\n                };\n            },\n        });\n\n        this.openMany2xRecord = useOpenMany2XRecord({\n            resModel: this.relation,\n            activeActions: {\n                create: false,\n                write: true,\n            },\n            onRecordSaved: async (record) => {\n                await this.props.record.data[this.props.name].forget(record);\n                return saveRecord([record.resId]);\n            },\n        });\n\n        this.update = (recordlist) => {\n            recordlist = recordlist\n                ? recordlist.filter((element) => {\n                      return !this.tags.some((record) => record.resId === element.id);\n                  })\n                : [];\n            if (!recordlist.length) {\n                return;\n            }\n            const resIds = recordlist.map((rec) => rec.id);\n            return saveRecord(resIds);\n        };\n\n        if (this.props.canQuickCreate) {\n            this.quickCreate = async (name) => {\n                const created = await this.orm.call(this.relation, \"name_create\", [name], {\n                    context: this.props.context,\n                });\n                return saveRecord([created[0]]);\n            };\n        }\n    }\n\n    get relation() {\n        return this.props.record.fields[this.props.name].relation;\n    }\n    get evalContext() {\n        return this.props.record.evalContext;\n    }\n    get string() {\n        return this.props.string || this.props.record.fields[this.props.name].string || \"\";\n    }\n\n    getTagProps(record) {\n        return {\n            id: record.id, // datapoint_X\n            resId: record.resId,\n            text: record.data.display_name,\n            colorIndex: record.data[this.props.colorField],\n            canEdit: this.props.canEditTags,\n            onDelete: !this.props.readonly ? () => this.deleteTag(record.id) : undefined,\n            onKeydown: (ev) => {\n                if (this.props.readonly) {\n                    return;\n                }\n                this.onTagKeydown(ev);\n            },\n        };\n    }\n\n    get tags() {\n        return this.props.record.data[this.props.name].records.map((record) =>\n            this.getTagProps(record)\n        );\n    }\n\n    get showM2OSelectionField() {\n        return !this.props.readonly;\n    }\n\n    async deleteTagByIndex(index) {\n        this.mutex.exec(() => {\n            if (this.tags[index]) {\n                return this.deleteTag(this.tags[index].id);\n            }\n        });\n    }\n\n    async deleteTag(id) {\n        const tagRecord = this.props.record.data[this.props.name].records.find(\n            (record) => record.id === id\n        );\n        await this.props.record.data[this.props.name].forget(tagRecord);\n    }\n\n    getDomain() {\n        return Domain.and([\n            getFieldDomain(this.props.record, this.props.name, this.props.domain),\n        ]).toList(this.props.context);\n    }\n\n    getOptionClassnames(record) {\n        const records = this.props.record.data[this.props.name].records;\n        const isSelected = records.some((r) => r.resId === record.id);\n        return isSelected ? \"dropdown-item-selected\" : \"\";\n    }\n}\n\nexport const many2ManyTagsField = {\n    component: Many2ManyTagsField,\n    displayName: _t(\"Tags\"),\n    supportedOptions: [\n        {\n            label: _t(\"Disable creation\"),\n            name: \"no_create\",\n            type: \"boolean\",\n            help: _t(\n                \"If checked, users won't be able to create records through the autocomplete dropdown at all.\"\n            ),\n        },\n        {\n            label: _t(\"Disable 'Create' option\"),\n            name: \"no_quick_create\",\n            type: \"boolean\",\n            help: _t(\n                \"If checked, users will not be able to create records based on the text input; they will still be able to create records via a popup form.\"\n            ),\n        },\n        {\n            label: _t(\"Disable 'Create and Edit' option\"),\n            name: \"no_create_edit\",\n            type: \"boolean\",\n            help: _t(\n                \"If checked, users will not be able to create records based through a popup form; they will still be able to create records based on the text input.\"\n            ),\n        },\n        {\n            label: _t(\"Can create\"),\n            name: \"create\",\n            type: \"string\",\n            help: _t(\"Write a domain to allow the creation of records conditionnally.\"),\n        },\n        {\n            label: _t(\"Color field\"),\n            name: \"color_field\",\n            type: \"field\",\n            availableTypes: [\"integer\"],\n            help: _t(\"Set an integer field to use colors with the tags.\"),\n        },\n    ],\n    supportedTypes: [\"many2many\"],\n    relatedFields: ({ options }) => {\n        const relatedFields = [{ name: \"display_name\", type: \"char\" }];\n        if (options.color_field) {\n            relatedFields.push({ name: options.color_field, type: \"integer\", readonly: false });\n        }\n        return relatedFields;\n    },\n    extractProps({ attrs, options, string }, dynamicInfo) {\n        const hasCreatePermission = attrs.can_create ? evaluateBooleanExpr(attrs.can_create) : true;\n        const noCreate = Boolean(options.no_create);\n        const canCreate = noCreate ? false : hasCreatePermission;\n        const noQuickCreate = Boolean(options.no_quick_create);\n        const noCreateEdit = Boolean(options.no_create_edit);\n        return {\n            colorField: options.color_field,\n            nameCreateField: options.create_name_field,\n            canCreate,\n            canQuickCreate: canCreate && !noQuickCreate,\n            canCreateEdit: canCreate && !noCreateEdit,\n            createDomain: options.create,\n            context: dynamicInfo.context,\n            domain: dynamicInfo.domain,\n            placeholder: attrs.placeholder,\n            string,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"many2many_tags\", many2ManyTagsField);\nregistry.category(\"fields\").add(\"calendar.one2many\", many2ManyTagsField);\nregistry.category(\"fields\").add(\"calendar.many2many\", many2ManyTagsField);\n\n/**\n * A specialization that allows to edit the color with the colorpicker.\n * Used in form view.\n */\nexport class Many2ManyTagsFieldColorEditable extends Many2ManyTagsField {\n    static components = {\n        ...super.components,\n        Popover: Many2ManyTagsFieldColorListPopover,\n    };\n    static props = {\n        ...super.props,\n        canEditColor: { type: Boolean, optional: true },\n        canEditTags: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        ...super.defaultProps,\n        canEditColor: true,\n        canEditTags: false,\n    };\n\n    getTagProps(record) {\n        const props = super.getTagProps(record);\n        props.onClick = (ev) => this.onTagClick(ev, record);\n        return props;\n    }\n\n    onTagClick(ev, record) {\n        if (this.props.canEditTags) {\n            return this.openMany2xRecord({\n                resId: record.resId,\n                context: this.props.context,\n                title: _t(\"Edit: %s\", record.data.display_name),\n            });\n        }\n        if (!this.props.canEditColor) {\n            return;\n        }\n        if (this.popover.isOpen) {\n            this.popover.close();\n        } else {\n            this.popover.open(ev.currentTarget, {\n                colors: this.constructor.RECORD_COLORS,\n                tag: {\n                    id: record.id,\n                    colorIndex: record.data[this.props.colorField],\n                },\n                switchTagColor: this.switchTagColor.bind(this),\n                onTagVisibilityChange: this.onTagVisibilityChange.bind(this),\n            });\n        }\n    }\n\n    async onTagVisibilityChange(isHidden, tag) {\n        const tagRecord = this.props.record.data[this.props.name].records.find(\n            (record) => record.id === tag.id\n        );\n        if (tagRecord.data[this.props.colorField] != 0) {\n            this.previousColorsMap[tagRecord.resId] = tagRecord.data[this.props.colorField];\n        }\n        const changes = {\n            [this.props.colorField]: isHidden ? 0 : this.previousColorsMap[tagRecord.resId] || 1,\n        };\n        await tagRecord.update(changes);\n        await tagRecord.save();\n        this.popover.close();\n    }\n\n    async switchTagColor(colorIndex, tag) {\n        const tagRecord = this.props.record.data[this.props.name].records.find(\n            (record) => record.id === tag.id\n        );\n        await tagRecord.update({ [this.props.colorField]: colorIndex });\n        await tagRecord.save();\n        this.popover.close();\n    }\n}\n\nexport const many2ManyTagsFieldColorEditable = {\n    ...many2ManyTagsField,\n    component: Many2ManyTagsFieldColorEditable,\n    supportedOptions: [\n        ...many2ManyTagsField.supportedOptions,\n        {\n            label: _t(\"Prevent color edition\"),\n            name: \"no_edit_color\",\n            type: \"boolean\",\n        },\n        {\n            label: _t(\"Edit Tags\"),\n            name: \"edit_tags\",\n            type: \"boolean\",\n            help: _t(\n                \"If checked, clicking on the tag will open the form that allows to directly edit it. Note that if a color field is also set, the tag edition will prevail. So, the color picker will not be displayed on click on the tag.\"\n            ),\n        },\n    ],\n    extractProps({ options, attrs }) {\n        const props = many2ManyTagsField.extractProps(...arguments);\n        const hasEditPermission = attrs.can_write ? evaluateBooleanExpr(attrs.can_write) : true;\n        props.canEditTags = options.edit_tags ? hasEditPermission : false;\n        props.canEditColor = !props.canEditTags && !options.no_edit_color && !!options.color_field;\n        return props;\n    },\n};\n\nregistry.category(\"fields\").add(\"form.many2many_tags\", many2ManyTagsFieldColorEditable);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { registry } from \"@web/core/registry\";\nimport {\n    many2ManyTagsField,\n    Many2ManyTagsField,\n} from \"@web/views/fields/many2many_tags/many2many_tags_field\";\nimport { TagsList } from \"@web/core/tags_list/tags_list\";\nimport { AvatarMany2XAutocomplete } from \"@web/views/fields/relational_utils\";\nimport { imageUrl } from \"@web/core/utils/urls\";\n\nexport class Many2ManyTagsAvatarField extends Many2ManyTagsField {\n    static template = \"web.Many2ManyTagsAvatarField\";\n    static components = {\n        Many2XAutocomplete: AvatarMany2XAutocomplete,\n        TagsList,\n    };\n    static props = {\n        ...Many2ManyTagsField.props,\n        withCommand: { type: Boolean, optional: true },\n    };\n    getTagProps(record) {\n        return {\n            ...super.getTagProps(record),\n            img: imageUrl(this.relation, record.resId, \"avatar_128\"),\n        };\n    }\n}\n\nexport const many2ManyTagsAvatarField = {\n    ...many2ManyTagsField,\n    component: Many2ManyTagsAvatarField,\n    extractProps({ viewType }, dynamicInfo) {\n        const props = many2ManyTagsField.extractProps(...arguments);\n        props.withCommand = viewType === \"form\" || viewType === \"list\";\n        props.domain = dynamicInfo.domain;\n        return props;\n    },\n};\n\nregistry.category(\"fields\").add(\"many2many_tags_avatar\", many2ManyTagsAvatarField);\n\nexport class ListMany2ManyTagsAvatarField extends Many2ManyTagsAvatarField {\n    visibleItemsLimit = 5;\n}\n\nexport const listMany2ManyTagsAvatarField = {\n    ...many2ManyTagsAvatarField,\n    component: ListMany2ManyTagsAvatarField,\n};\n\nregistry.category(\"fields\").add(\"list.many2many_tags_avatar\", listMany2ManyTagsAvatarField);\n\nexport class Many2ManyTagsAvatarFieldPopover extends Many2ManyTagsAvatarField {\n    static template = \"web.Many2ManyTagsAvatarFieldPopover\";\n    static props = {\n        ...Many2ManyTagsAvatarField.props,\n        close: { type: Function },\n    };\n\n    setup() {\n        super.setup();\n        const originalUpdate = this.update;\n        this.update = async (recordList) => {\n            await originalUpdate(recordList);\n            await this._saveUpdate();\n        };\n    }\n\n    async deleteTag(id) {\n        await super.deleteTag(id);\n        await this._saveUpdate();\n    }\n\n    async _saveUpdate() {\n        await this.props.record.save({ reload: false });\n        // manual render to dirty record\n        this.render();\n        // update dropdown\n        this.autoCompleteRef.el?.querySelector(\"input\")?.click();\n    }\n\n    get tags() {\n        return super.tags.reverse();\n    }\n}\n\nexport const many2ManyTagsAvatarFieldPopover = {\n    ...many2ManyTagsAvatarField,\n    component: Many2ManyTagsAvatarFieldPopover,\n};\nregistry.category(\"fields\").add(\"many2many_tags_avatar_popover\", many2ManyTagsAvatarFieldPopover);\n\nexport class KanbanMany2ManyTagsAvatarFieldTagsList extends TagsList {\n    static template = \"web.KanbanMany2ManyTagsAvatarFieldTagsList\";\n\n    static props = {\n        ...TagsList.props,\n        popoverProps: { type: Object },\n        readonly: { type: Boolean, optional: true },\n    };\n    setup() {\n        super.setup();\n        this.popover = usePopover(Many2ManyTagsAvatarFieldPopover, {\n            popoverClass: \"o_m2m_tags_avatar_field_popover\",\n            closeOnClickAway: (target) => !target.closest(\".modal\"),\n        });\n    }\n    openPopover(ev) {\n        if (this.props.readonly) {\n            return;\n        }\n        this.popover.open(ev.currentTarget.parentElement, {\n            ...this.props.popoverProps,\n            readonly: false,\n            canCreate: false,\n            canCreateEdit: false,\n            canQuickCreate: false,\n            placeholder: _t(\"Search users...\"),\n        });\n    }\n    get canDisplayQuickAssignAvatar() {\n        return !this.props.readonly && !(this.props.tags && this.otherTags.length);\n    }\n}\n\nexport class KanbanMany2ManyTagsAvatarField extends Many2ManyTagsAvatarField {\n    static template = \"web.KanbanMany2ManyTagsAvatarField\";\n    static components = {\n        ...Many2ManyTagsAvatarField.components,\n        TagsList: KanbanMany2ManyTagsAvatarFieldTagsList,\n    };\n    static props = {\n        ...Many2ManyTagsAvatarField.props,\n        isEditable: { type: Boolean, optional: true },\n    };\n    visibleItemsLimit = 3;\n\n    get popoverProps() {\n        const props = {\n            ...this.props,\n            readonly: false,\n        };\n        delete props.isEditable;\n        return props;\n    }\n    get tags() {\n        return super.tags.reverse();\n    }\n}\n\nexport const kanbanMany2ManyTagsAvatarField = {\n    ...many2ManyTagsAvatarField,\n    component: KanbanMany2ManyTagsAvatarField,\n    extractProps(fieldInfo, dynamicInfo) {\n        const props = many2ManyTagsAvatarField.extractProps(...arguments);\n        props.isEditable = !dynamicInfo.readonly;\n        return props;\n    },\n};\n\nregistry.category(\"fields\").add(\"kanban.many2many_tags_avatar\", kanbanMany2ManyTagsAvatarField);\n", "import { browser } from \"@web/core/browser/browser\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { makeContext } from \"@web/core/context\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { registry } from \"@web/core/registry\";\nimport { useChildRef, useOwnedDialogs, useService } from \"@web/core/utils/hooks\";\nimport { escape, sprintf } from \"@web/core/utils/strings\";\nimport { Many2XAutocomplete, useOpenMany2XRecord } from \"@web/views/fields/relational_utils\";\nimport * as BarcodeScanner from \"@web/core/barcode/barcode_dialog\";\nimport { isBarcodeScannerSupported } from \"@web/core/barcode/barcode_video_scanner\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component, markup, onWillUpdateProps, useState } from \"@odoo/owl\";\nimport { getFieldDomain } from \"@web/model/relational_model/utils\";\n\nclass CreateConfirmationDialog extends Component {\n    static template = \"web.Many2OneField.CreateConfirmationDialog\";\n    static components = { Dialog };\n    static props = {\n        name: String,\n        value: String,\n        create: Function,\n        close: Function,\n    };\n\n    get title() {\n        return _t(\"New: %s\", this.props.name);\n    }\n\n    get dialogContent() {\n        return markup(\n            sprintf(escape(_t(\"Create %(value)s as a new %(field)s?\")), {\n                value: `<strong>${escape(this.props.value)}</strong>`,\n                field: escape(this.props.name),\n            })\n        );\n    }\n\n    async onCreate() {\n        await this.props.create();\n        this.props.close();\n    }\n}\n\nexport function m2oTupleFromData(data) {\n    const id = data.id;\n    let name;\n    if (\"display_name\" in data) {\n        name = data.display_name;\n    } else {\n        const _name = data.name;\n        name = Array.isArray(_name) ? _name[1] : _name;\n    }\n    return [id, name];\n}\n\nexport class Many2OneField extends Component {\n    static template = \"web.Many2OneField\";\n    static components = {\n        Many2XAutocomplete,\n    };\n    static props = {\n        ...standardFieldProps,\n        placeholder: { type: String, optional: true },\n        canOpen: { type: Boolean, optional: true },\n        canCreate: { type: Boolean, optional: true },\n        canWrite: { type: Boolean, optional: true },\n        canQuickCreate: { type: Boolean, optional: true },\n        canCreateEdit: { type: Boolean, optional: true },\n        context: { type: Object, optional: true },\n        openActionContext: { type: String, optional: true },\n        domain: { type: [Array, Function], optional: true },\n        nameCreateField: { type: String, optional: true },\n        searchLimit: { type: Number, optional: true },\n        relation: { type: String, optional: true },\n        string: { type: String, optional: true },\n        canScanBarcode: { type: Boolean, optional: true },\n        update: { type: Function, optional: true },\n        value: { optional: true },\n        decorations: { type: Object, optional: true },\n    };\n    static defaultProps = {\n        canOpen: true,\n        canCreate: true,\n        canWrite: true,\n        canQuickCreate: true,\n        canCreateEdit: true,\n        nameCreateField: \"name\",\n        searchLimit: 7,\n        string: \"\",\n        canScanBarcode: false,\n        context: {},\n        decorations: {},\n    };\n\n    static SEARCH_MORE_LIMIT = 320;\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n        this.dialog = useService(\"dialog\");\n        this.notification = useService(\"notification\");\n        this.autocompleteContainerRef = useChildRef();\n        this.addDialog = useOwnedDialogs();\n\n        this.focusInput = () => {\n            this.autocompleteContainerRef.el.querySelector(\"input\").focus();\n        };\n\n        this.state = useState({\n            isFloating: false,\n        });\n        this.computeActiveActions(this.props);\n\n        this.openMany2X = useOpenMany2XRecord({\n            resModel: this.relation,\n            activeActions: this.state.activeActions,\n            isToMany: false,\n            onRecordSaved: async (record) => {\n                const resId = this.value[0];\n                const fields = [\"display_name\"];\n                // use unity read + relatedFields from Field Component\n                const records = await this.orm.read(this.relation, [resId], fields, {\n                    context: this.context,\n                });\n                await this.updateRecord(m2oTupleFromData(records[0]));\n            },\n            onClose: () => this.focusInput(),\n            fieldString: this.string,\n        });\n\n        this.update = (value, params = {}) => {\n            if (value) {\n                value = m2oTupleFromData(value[0]);\n            }\n            this.state.isFloating = false;\n            return this.updateRecord(value);\n        };\n\n        if (this.props.canQuickCreate) {\n            this.quickCreate = (name) => {\n                this.state.isFloating = false;\n                return this.updateRecord([false, name]);\n            };\n        }\n\n        this.setFloating = (bool) => {\n            this.state.isFloating = bool;\n        };\n\n        onWillUpdateProps(async (nextProps) => {\n            this.computeActiveActions(nextProps);\n        });\n    }\n\n    updateRecord(value) {\n        const changes = { [this.props.name]: value };\n        if (this.props.update) {\n            return this.props.update(changes);\n        }\n        return this.props.record.update(changes);\n    }\n\n    get relation() {\n        return this.props.relation || this.props.record.fields[this.props.name].relation;\n    }\n    get urlRelation() {\n        if (!this.relation.includes(\".\")) {\n            return \"m-\" + this.relation;\n        }\n        return this.relation;\n    }\n    get string() {\n        return this.props.string || this.props.record.fields[this.props.name].string || \"\";\n    }\n    get hasExternalButton() {\n        return this.props.canOpen && !!this.value && !this.state.isFloating;\n    }\n    get context() {\n        return this.props.context;\n    }\n    get classFromDecoration() {\n        const evalContext = this.props.record.evalContextWithVirtualIds;\n        for (const decorationName in this.props.decorations) {\n            if (evaluateBooleanExpr(this.props.decorations[decorationName], evalContext)) {\n                return `text-${decorationName}`;\n            }\n        }\n        return \"\";\n    }\n    get displayName() {\n        if (this.value && this.value[1]) {\n            return this.value[1].split(\"\\n\")[0];\n        } else if (this.value) {\n            return _t(\"Unnamed\");\n        } else {\n            return \"\";\n        }\n    }\n    get extraLines() {\n        return this.value && this.value[1]\n            ? this.value[1]\n                  .split(\"\\n\")\n                  .map((line) => line.trim())\n                  .slice(1)\n            : [];\n    }\n    get resId() {\n        return this.value && this.value[0];\n    }\n    get value() {\n        return \"value\" in this.props ? this.props.value : this.props.record.data[this.props.name];\n    }\n    get Many2XAutocompleteProps() {\n        return {\n            value: this.displayName,\n            id: this.props.id,\n            placeholder: this.props.placeholder,\n            resModel: this.relation,\n            autoSelect: true,\n            fieldString: this.string,\n            activeActions: this.state.activeActions,\n            update: this.update,\n            quickCreate: this.quickCreate,\n            context: this.context,\n            getDomain: this.getDomain.bind(this),\n            nameCreateField: this.props.nameCreateField,\n            setInputFloats: this.setFloating,\n            autocomplete_container: this.autocompleteContainerRef,\n        };\n    }\n    computeActiveActions(props) {\n        this.state.activeActions = {\n            create: props.canCreate,\n            createEdit: props.canCreateEdit,\n            write: props.canWrite,\n        };\n    }\n    getDomain() {\n        return getFieldDomain(this.props.record, this.props.name, this.props.domain);\n    }\n    async openAction() {\n        const { name, openActionContext, record } = this.props;\n        const context = makeContext(\n            [openActionContext || this.context, record.fields[name].context],\n            record.evalContext\n        );\n        const action = await this.orm.call(this.relation, \"get_formview_action\", [[this.resId]], {\n            context,\n        });\n        await this.action.doAction(action);\n    }\n    async openDialog(resId) {\n        return this.openMany2X({ resId, context: this.context });\n    }\n\n    async openConfirmationDialog(request) {\n        return new Promise((resolve, reject) => {\n            this.addDialog(CreateConfirmationDialog, {\n                value: request,\n                name: this.string,\n                create: async () => {\n                    try {\n                        await this.quickCreate(request);\n                        resolve();\n                    } catch (e) {\n                        reject(e);\n                    }\n                },\n            });\n        });\n    }\n\n    onClick(ev) {\n        if (this.props.canOpen && this.props.readonly) {\n            ev.stopPropagation();\n            this.openAction();\n        }\n    }\n    onExternalBtnClick() {\n        if (this.env.inDialog) {\n            this.openDialog(this.resId);\n        } else {\n            this.openAction();\n        }\n    }\n    async onBarcodeBtnClick() {\n        const barcode = await BarcodeScanner.scanBarcode(this.env);\n        if (barcode) {\n            await this.onBarcodeScanned(barcode);\n            if (\"vibrate\" in browser.navigator) {\n                browser.navigator.vibrate(100);\n            }\n        } else {\n            this.notification.add(_t(\"Please, scan again!\"), {\n                type: \"warning\",\n            });\n        }\n    }\n    async search(barcode) {\n        const results = await this.orm.call(this.relation, \"name_search\", [], {\n            name: barcode,\n            args: this.getDomain(),\n            operator: \"ilike\",\n            limit: 2, // If one result we set directly and if more than one we use normal flow so no need to search more\n            context: this.context,\n        });\n        return results.map((result) => {\n            const [id, displayName] = result;\n            return {\n                id,\n                name: displayName,\n            };\n        });\n    }\n    async onBarcodeScanned(barcode) {\n        const results = await this.search(barcode);\n        const records = results.filter((r) => !!r.id);\n        if (records.length === 1) {\n            this.update([{ id: records[0].id, name: records[0].name }]);\n        } else {\n            const searchInput = this.autocompleteContainerRef.el.querySelector(\"input\");\n            searchInput.value = barcode;\n            searchInput.dispatchEvent(new Event(\"input\"));\n            if (this.env.isSmall) {\n                searchInput.dispatchEvent(new Event(\"barcode-search\"));\n            }\n        }\n    }\n    get hasBarcodeButton() {\n        const canScanBarcode = this.props.canScanBarcode;\n        const supported = isBarcodeScannerSupported();\n        return canScanBarcode && isMobileOS() && supported && !this.hasExternalButton;\n    }\n}\n\nexport const many2OneField = {\n    component: Many2OneField,\n    displayName: _t(\"Many2one\"),\n    supportedOptions: [\n        {\n            label: _t(\"Disable opening\"),\n            name: \"no_open\",\n            type: \"boolean\",\n        },\n        {\n            label: _t(\"Disable creation\"),\n            name: \"no_create\",\n            type: \"boolean\",\n            help: _t(\n                \"If checked, users won't be able to create records through the autocomplete dropdown at all.\"\n            ),\n        },\n        {\n            label: _t(\"Disable 'Create' option\"),\n            name: \"no_quick_create\",\n            type: \"boolean\",\n            help: _t(\n                \"If checked, users will not be able to create records based on the text input; they will still be able to create records via a popup form.\"\n            ),\n        },\n        {\n            label: _t(\"Disable 'Create and Edit' option\"),\n            name: \"no_create_edit\",\n            type: \"boolean\",\n            help: _t(\n                \"If checked, users will not be able to create records based through a popup form; they will still be able to create records based on the text input.\"\n            ),\n        },\n    ],\n    supportedTypes: [\"many2one\"],\n    extractProps({ attrs, context, decorations, options, string }, dynamicInfo) {\n        const hasCreatePermission = attrs.can_create ? evaluateBooleanExpr(attrs.can_create) : true;\n        const hasWritePermission = attrs.can_write ? evaluateBooleanExpr(attrs.can_write) : true;\n        const canCreate = options.no_create ? false : hasCreatePermission;\n        return {\n            placeholder: attrs.placeholder,\n            canOpen: !options.no_open,\n            canCreate,\n            canWrite: hasWritePermission,\n            canQuickCreate: canCreate && !options.no_quick_create,\n            canCreateEdit: canCreate && !options.no_create_edit,\n            context: dynamicInfo.context,\n            openActionContext: context || \"{}\",\n            decorations,\n            domain: dynamicInfo.domain,\n            nameCreateField: options.create_name_field,\n            canScanBarcode: !!options.can_scan_barcode,\n            string,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"many2one\", many2OneField);\n// the two following lines are there to prevent the fallback on legacy widgets\nregistry.category(\"fields\").add(\"list.many2one\", many2OneField);\nregistry.category(\"fields\").add(\"kanban.many2one\", many2OneField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { many2OneField, Many2OneField } from \"../many2one/many2one_field\";\n\nimport { AvatarMany2XAutocomplete } from \"@web/views/fields/relational_utils\";\n\nexport class Many2OneAvatarField extends Many2OneField {\n    static template = \"web.Many2OneAvatarField\";\n    static components = {\n        ...Many2OneField.components,\n        Many2XAutocomplete: AvatarMany2XAutocomplete,\n    };\n}\n\nexport const many2OneAvatarField = {\n    ...many2OneField,\n    component: Many2OneAvatarField,\n    extractProps(fieldInfo) {\n        const props = many2OneField.extractProps(...arguments);\n        props.canOpen = fieldInfo.viewType === \"form\";\n        return props;\n    },\n};\n\nexport class Many2OneFieldPopover extends Many2OneField {\n    static props = {\n        ...Many2OneField.props,\n        close: { type: Function },\n    };\n    static components = {\n        Many2XAutocomplete: AvatarMany2XAutocomplete,\n    };\n    get Many2XAutocompleteProps() {\n        return {\n            ...super.Many2XAutocompleteProps,\n            dropdown: false,\n            autofocus: true,\n        };\n    }\n\n    async updateRecord(value) {\n        const updatedValue = await super.updateRecord(...arguments);\n        await this.props.record.save();\n        return updatedValue;\n    }\n}\n\nexport class KanbanMany2OneAvatarField extends Many2OneAvatarField {\n    static template = \"web.KanbanMany2OneAvatarField\";\n    static props = {\n        ...Many2OneAvatarField.props,\n        isEditable: { type: Boolean, optional: true },\n    };\n    setup() {\n        super.setup();\n        this.popover = usePopover(Many2OneFieldPopover, {\n            popoverClass: \"o_m2o_tags_avatar_field_popover\",\n            closeOnClickAway: (target) => !target.closest(\".modal\"),\n        });\n    }\n    get popoverProps() {\n        const props = {\n            ...this.props,\n            readonly: false,\n        };\n        delete props.isEditable;\n        return props;\n    }\n    openPopover(ev) {\n        if (!this.props.isEditable) {\n            return;\n        }\n        this.popover.open(ev.currentTarget, {\n            ...this.popoverProps,\n            canCreate: false,\n            canCreateEdit: false,\n            canQuickCreate: false,\n            placeholder: _t(\"Search user...\"),\n        });\n    }\n}\n\nexport const kanbanMany2OneAvatarField = {\n    ...many2OneField,\n    component: KanbanMany2OneAvatarField,\n    additionalClasses: [\"o_field_many2one_avatar_kanban\"],\n    extractProps(fieldInfo, dynamicInfo) {\n        const props = many2OneAvatarField.extractProps(...arguments);\n        props.isEditable = !dynamicInfo.readonly;\n        return props;\n    },\n};\nregistry.category(\"fields\").add(\"many2one_avatar\", many2OneAvatarField);\nregistry.category(\"fields\").add(\"kanban.many2one_avatar\", kanbanMany2OneAvatarField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { many2OneField, Many2OneField } from \"../many2one/many2one_field\";\n\nexport class Many2OneBarcodeField extends Many2OneField {\n    static defaultProps = {\n        ...super.defaultProps,\n        canScanBarcode: true,\n    };\n}\n\nexport const many2OneBarcodeField = {\n    ...many2OneField,\n    component: Many2OneBarcodeField,\n    displayName: _t(\"Many2OneBarcode\"),\n    extractProps() {\n        const props = many2OneField.extractProps(...arguments);\n        props.canScanBarcode = true;\n        return props;\n    },\n};\n\nregistry.category(\"fields\").add(\"many2one_barcode\", many2OneBarcodeField);\n", "import { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { many2OneField, Many2OneField } from \"@web/views/fields/many2one/many2one_field\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class Many2OneReferenceField extends Component {\n    static template = \"web.Many2OneReferenceField\";\n    static components = { Many2OneField };\n    static props = Many2OneField.props;\n\n    get relation() {\n        const modelField = this.props.record.fields[this.props.name].model_field;\n        if (!(modelField in this.props.record.data)) {\n            throw new Error(`Many2OneReferenceField: model_field must be in view (${modelField})`);\n        }\n        return this.props.record.data[modelField];\n    }\n\n    get m2oProps() {\n        const relation = this.relation;\n        const value = this.props.record.data[this.props.name];\n        return {\n            ...this.props,\n            relation,\n            value: value ? [value.resId, value.displayName] : false,\n            readonly: this.props.readonly || !relation,\n            update: (changes) => {\n                let nextVal;\n                if (changes[this.props.name]) {\n                    nextVal = {\n                        resId: changes[this.props.name][0],\n                        displayName: changes[this.props.name][1],\n                    };\n                } else {\n                    nextVal = false;\n                }\n                return this.props.record.update({ [this.props.name]: nextVal });\n            },\n        };\n    }\n}\n\nconst many2oneReferenceField = {\n    component: Many2OneReferenceField,\n    displayName: _t(\"Many2OneReference\"),\n    relatedFields: [{ name: \"display_name\", type: \"char\" }],\n    supportedTypes: [\"many2one_reference\"],\n    extractProps: many2OneField.extractProps,\n};\n\nregistry.category(\"fields\").add(\"many2one_reference\", many2oneReferenceField);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { IntegerField } from \"@web/views/fields/integer/integer_field\";\n\nexport class Many2OneReferenceIntegerField extends IntegerField {\n    get value() {\n        const value = this.props.record.data[this.props.name];\n        return value ? value.resId : false;\n    }\n}\n\nconst many2oneReferenceIntegerField = {\n    component: Many2OneReferenceIntegerField,\n    displayName: _t(\"Many2OneReferenceInteger\"),\n    supportedTypes: [\"many2one_reference\"],\n};\n\nregistry.category(\"fields\").add(\"many2one_reference_integer\", many2oneReferenceIntegerField);\n", "import { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { formatMonetary } from \"../formatters\";\nimport { parseMonetary } from \"../parsers\";\nimport { useInputField } from \"../input_field_hook\";\nimport { useNumpadDecimal } from \"../numpad_decimal_hook\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { nbsp } from \"@web/core/utils/strings\";\n\nimport { Component, useState, useEffect } from \"@odoo/owl\";\nimport { getCurrency } from \"@web/core/currency\";\n\nexport class MonetaryField extends Component {\n    static template = \"web.MonetaryField\";\n    static props = {\n        ...standardFieldProps,\n        currencyField: { type: String, optional: true },\n        inputType: { type: String, optional: true },\n        useFieldDigits: { type: Boolean, optional: true },\n        hideSymbol: { type: Boolean, optional: true },\n        placeholder: { type: String, optional: true },\n    };\n    static defaultProps = {\n        hideSymbol: false,\n        inputType: \"text\",\n    };\n\n    setup() {\n        this.inputRef = useInputField(this.inputOptions);\n        this.state = useState({ value: undefined });\n        this.nbsp = nbsp;\n        useNumpadDecimal();\n        useEffect(() => {\n            if (this.inputRef?.el) {\n                this.state.value = this.inputRef.el.value;\n            }\n        });\n    }\n\n    get inputOptions() {\n        return {\n            getValue: () => this.formattedValue,\n            refName: \"numpadDecimal\",\n            parse: parseMonetary,\n        };\n    }\n\n    get currencyId() {\n        const currencyField =\n            this.props.currencyField ||\n            this.props.record.fields[this.props.name].currency_field ||\n            \"currency_id\";\n        const currency = this.props.record.data[currencyField];\n        return currency && currency[0];\n    }\n    get currency() {\n        if (!isNaN(this.currencyId)) {\n            return getCurrency(this.currencyId) || null;\n        }\n        return null;\n    }\n\n    get currencySymbol() {\n        return this.currency ? this.currency.symbol : \"\";\n    }\n\n    get currencyDigits() {\n        if (this.props.useFieldDigits) {\n            return this.props.record.fields[this.props.name].digits;\n        }\n        if (!this.currency) {\n            return null;\n        }\n        return getCurrency(this.currencyId).digits;\n    }\n\n    get value() {\n        return this.props.record.data[this.props.name];\n    }\n\n    get formattedValue() {\n        if (this.props.inputType === \"number\" && !this.props.readonly && this.value) {\n            return this.value;\n        }\n        return formatMonetary(this.value, {\n            digits: this.currencyDigits,\n            currencyId: this.currencyId,\n            noSymbol: !this.props.readonly || this.props.hideSymbol,\n        });\n    }\n\n    onInput(ev) {\n        this.state.value = ev.target.value;\n    }\n}\n\nexport const monetaryField = {\n    component: MonetaryField,\n    supportedOptions: [\n        {\n            label: _t(\"Hide symbol\"),\n            name: \"no_symbol\",\n            type: \"boolean\",\n        },\n        {\n            label: _t(\"Currency\"),\n            name: \"currency_field\",\n            type: \"field\",\n            availableTypes: [\"many2one\"],\n        },\n    ],\n    supportedTypes: [\"monetary\", \"float\"],\n    displayName: _t(\"Monetary\"),\n    extractProps: ({ attrs, options }) => ({\n        currencyField: options.currency_field,\n        inputType: attrs.type,\n        useFieldDigits: options.field_digits,\n        hideSymbol: options.no_symbol,\n        placeholder: attrs.placeholder,\n    }),\n};\n\nregistry.category(\"fields\").add(\"monetary\", monetaryField);\n", "import { localization } from \"@web/core/l10n/localization\";\nimport { isIOS } from \"@web/core/browser/feature_detection\";\n\nimport { useRef, useEffect } from \"@odoo/owl\";\n\nfunction onKeydown(ev) {\n    const decimalPoint = localization.decimalPoint;\n    if (\n        !([\".\", \",\"].includes(ev.key) && ev.code === \"NumpadDecimal\") ||\n        ev.key === decimalPoint ||\n        ev.target.type === \"number\"\n    ) {\n        return;\n    }\n    ev.preventDefault();\n    ev.target.setRangeText(decimalPoint, ev.target.selectionStart, ev.target.selectionEnd, \"end\");\n}\n\nfunction onFocus(ev) {\n    ev.target.select();\n}\n\n/**\n * This hook replaces the decimal separator of the numpad decimal key\n * by the decimal separator from the user's language setting when user\n * edits an input. The input is found using a t-ref=\"numpadDecimal\"\n * reference in the current component. It can be placed directly on an\n * input or an element containing multiple inputs that require the\n * behavior\n *\n * NOTE: Special consideration for the input type = \"number\". In this\n * case, whatever the user types, we let the browser's default behavior.\n *\n * NOTE: On IOS devices, the inputmode attribute prevents the user from\n * entering a negative number (the minus sign is not on the virtual keyboard),\n * so we need to remove it.\n */\nexport function useNumpadDecimal() {\n    const ref = useRef(\"numpadDecimal\");\n    const isIOSDevice = isIOS();\n    useEffect(() => {\n        let inputs = [];\n        const el = ref.el;\n        if (el) {\n            inputs = el.nodeName === \"INPUT\" ? [el] : el.querySelectorAll(\"input\");\n            inputs.forEach((input) => input.addEventListener(\"keydown\", onKeydown));\n            inputs.forEach((input) => input.addEventListener(\"focus\", onFocus));\n            if (isIOSDevice) {\n                inputs.forEach((input) => input.removeAttribute(\"inputmode\"));\n            }\n        }\n        return () => {\n            inputs.forEach((input) => input.removeEventListener(\"keydown\", onKeydown));\n            inputs.forEach((input) => input.removeEventListener(\"focus\", onFocus));\n        };\n    });\n}\n", "import { parseDate, parseDateTime } from \"@web/core/l10n/dates\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { evaluateExpr } from \"@web/core/py_js/py\";\nimport { registry } from \"@web/core/registry\";\nimport { escapeRegExp } from \"@web/core/utils/strings\";\n\n// -----------------------------------------------------------------------------\n// Helpers\n// -----------------------------------------------------------------------------\n\nfunction evaluateMathematicalExpression(expr, context = {}) {\n    // remove extra space\n    var val = expr.replace(new RegExp(/( )/g), \"\");\n    var safeEvalString = \"\";\n    for (let v of val.split(new RegExp(/([-+*/()^])/g))) {\n        if (![\"+\", \"-\", \"*\", \"/\", \"(\", \")\", \"^\"].includes(v) && v.length) {\n            // check if this is a float and take into account user delimiter preference\n            v = parseFloat(v);\n        }\n        if (v === \"^\") {\n            v = \"**\";\n        }\n        safeEvalString += v;\n    }\n    return evaluateExpr(safeEvalString, context);\n}\n\n/**\n * Parses a string into a number.\n *\n * @param {string} value\n * @param {Object} options - additional options\n * @param {string|RegExp} options.thousandsSep - the thousands separator used in the value\n * @param {string|RegExp} options.decimalPoint - the decimal point used in the value\n * @returns {number}\n */\nfunction parseNumber(value, options = {}) {\n    if (value.startsWith(\"=\")) {\n        value = evaluateMathematicalExpression(value.substring(1));\n        if (options.truncate) {\n            value = Math.trunc(value);\n        }\n    } else {\n        // A whitespace thousands separator is equivalent to any whitespace character.\n        // E.g. \"1  000 000\" should be parsed as 1000000 even if the\n        // thousands separator is nbsp.\n        const thousandsSepRegex = options.thousandsSep.match(/\\s+/)\n            ? /\\s+/g\n            : new RegExp(escapeRegExp(options.thousandsSep), \"g\") || \",\";\n\n        // a number can have the thousand separator multiple times. ex: 1,000,000.00\n        value = value.replaceAll(thousandsSepRegex, \"\");\n        // a number only have one decimal separator\n        value = value.replace(new RegExp(escapeRegExp(options.decimalPoint), \"g\") || \".\", \".\");\n    }\n\n    return Number(value);\n}\n\n// -----------------------------------------------------------------------------\n// Exports\n// -----------------------------------------------------------------------------\n\nexport class InvalidNumberError extends Error {}\n\n/**\n * Try to extract a float from a string. The localization is considered in the process.\n *\n * @param {string} value\n * @returns {number} a float\n */\nexport function parseFloat(value) {\n    const thousandsSepRegex = localization.thousandsSep || \"\";\n    const decimalPointRegex = localization.decimalPoint;\n    let parsed = parseNumber(value, {\n        thousandsSep: thousandsSepRegex,\n        decimalPoint: decimalPointRegex,\n    });\n    if (isNaN(parsed)) {\n        parsed = parseNumber(value, {\n            thousandsSep: \",\",\n            decimalPoint: \".\",\n        });\n        if (isNaN(parsed)) {\n            throw new InvalidNumberError(`\"${value}\" is not a correct number`);\n        }\n    }\n    return parsed;\n}\n\n/**\n * Try to extract a float time from a string. The localization is considered in the process.\n * The float time can have two formats: float or integer:integer.\n *\n * @param {string} value\n * @returns {number} a float\n */\nexport function parseFloatTime(value) {\n    let sign = 1;\n    if (value[0] === \"-\") {\n        value = value.slice(1);\n        sign = -1;\n    }\n    const values = value.split(\":\");\n    if (values.length > 2) {\n        throw new InvalidNumberError(`\"${value}\" is not a correct number`);\n    }\n    if (values.length === 1) {\n        return sign * parseFloat(value);\n    }\n    const hours = parseInteger(values[0]);\n    const minutes = parseInteger(values[1]);\n    return sign * (hours + minutes / 60);\n}\n\n/**\n * Try to extract an integer from a string. The localization is considered in the process.\n *\n * @param {string} value\n * @returns {number} an integer\n */\nexport function parseInteger(value) {\n    const thousandsSepRegex = localization.thousandsSep || \"\";\n    const decimalPointRegex = localization.decimalPoint;\n    let parsed = parseNumber(value, {\n        thousandsSep: thousandsSepRegex,\n        decimalPoint: decimalPointRegex,\n        truncate: true,\n    });\n    if (!Number.isInteger(parsed)) {\n        parsed = parseNumber(value, {\n            thousandsSep: \",\",\n            decimalPoint: \".\",\n            truncate: true,\n        });\n        if (!Number.isInteger(parsed)) {\n            throw new InvalidNumberError(`\"${value}\" is not a correct number`);\n        }\n    }\n    if (parsed < -2147483648 || parsed > 2147483647) {\n        throw new InvalidNumberError(\n            `\"${value}\" is out of bounds (integers should be between -2,147,483,648 and 2,147,483,647)`\n        );\n    }\n    return parsed;\n}\n\n/**\n * Try to extract a float from a string and unconvert it with a conversion factor of 100.\n * The localization is considered in the process.\n * The percentage can have two formats: float or float%.\n *\n * @param {string} value\n * @returns {number} float\n */\nexport function parsePercentage(value) {\n    if (value[value.length - 1] === \"%\") {\n        value = value.slice(0, value.length - 1);\n    }\n    return parseFloat(value) / 100;\n}\n\n/**\n * Try to extract a monetary value from a string. The localization is considered in the process.\n * This is a very lenient function such that it ignores everything before we encounter a substring consisting of either\n * - a sign (- or +)\n * - an equals sign (signaling the start of a mathematical expression)\n * - a decimal point\n * - a number\n * We then remove any non-numeric characters at the end\n *\n *\n * @param {string} value\n * @returns {number}\n */\nexport function parseMonetary(value) {\n    value = value.trim();\n    const startMatch = value.match(\n        new RegExp(`[\\\\d\\\\-+=]|${escapeRegExp(localization.decimalPoint)}`)\n    );\n    if (startMatch) {\n        value = value.substring(startMatch.index);\n    }\n    value = value.replace(/\\D*$/, \"\");\n    return parseFloat(value);\n}\n\nregistry\n    .category(\"parsers\")\n    .add(\"date\", parseDate)\n    .add(\"datetime\", parseDateTime)\n    .add(\"float\", parseFloat)\n    .add(\"float_time\", parseFloatTime)\n    .add(\"integer\", parseInteger)\n    .add(\"many2one_reference\", parseInteger)\n    .add(\"monetary\", parseMonetary)\n    .add(\"percentage\", parsePercentage);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { url } from \"@web/core/utils/urls\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { FileUploader } from \"../file_handler\";\n\nimport { Component, onWillUpdateProps, useState } from \"@odoo/owl\";\n\nexport class PdfViewerField extends Component {\n    static template = \"web.PdfViewerField\";\n    static components = {\n        FileUploader,\n    };\n    static props = {\n        ...standardFieldProps,\n    };\n\n    setup() {\n        this.notification = useService(\"notification\");\n        this.state = useState({\n            isValid: true,\n            objectUrl: \"\",\n        });\n        onWillUpdateProps((nextProps) => {\n            if (nextProps.readonly) {\n                this.state.objectUrl = \"\";\n            }\n        });\n    }\n\n    get url() {\n        if (!this.state.isValid || !this.props.record.data[this.props.name]) {\n            return null;\n        }\n        const page = this.props.record.data[`${this.props.name}_page`] || 1;\n        const file = encodeURIComponent(\n            this.state.objectUrl ||\n                url(\"/web/content\", {\n                    model: this.props.record.resModel,\n                    field: this.props.name,\n                    id: this.props.record.resId,\n                })\n        );\n        return `/web/static/lib/pdfjs/web/viewer.html?file=${file}#page=${page}`;\n    }\n\n    update({ data }) {\n        const changes = { [this.props.name]: data || false };\n        return this.props.record.update(changes);\n    }\n\n    onFileRemove() {\n        this.state.isValid = true;\n        this.update({});\n    }\n\n    onFileUploaded({ data, objectUrl }) {\n        this.state.isValid = true;\n        this.state.objectUrl = objectUrl;\n        this.update({ data });\n    }\n\n    onLoadFailed() {\n        this.state.isValid = false;\n        this.notification.add(_t(\"Could not display the selected pdf\"), {\n            type: \"danger\",\n        });\n    }\n}\n\nexport const pdfViewerField = {\n    component: PdfViewerField,\n    displayName: _t(\"PDF Viewer\"),\n    supportedOptions: [\n        {\n            label: _t(\"Preview image\"),\n            name: \"preview_image\",\n            type: \"field\",\n            availableTypes: [\"binary\"],\n        },\n    ],\n    supportedTypes: [\"binary\"],\n};\n\nregistry.category(\"fields\").add(\"pdf_viewer\", pdfViewerField);\n", "import { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { formatFloat } from \"../formatters\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class PercentPieField extends Component {\n    static template = \"web.PercentPieField\";\n    static props = {\n        ...standardFieldProps,\n        string: { type: String, optional: true },\n    };\n\n    /**\n     * Format to 2 decimals without trailing zeros.\n     */\n    get formattedValue() {\n        return formatFloat(this.props.record.data[this.props.name], {\n            trailingZeros: false,\n        });\n    }\n}\n\nexport const percentPieField = {\n    component: PercentPieField,\n    displayName: _t(\"PercentPie\"),\n    supportedTypes: [\"float\", \"integer\"],\n    additionalClasses: [\"o_field_percent_pie\"],\n    extractProps: ({ string }) => ({ string }),\n};\n\nregistry.category(\"fields\").add(\"percentpie\", percentPieField);\n", "import { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { formatPercentage } from \"../formatters\";\nimport { parsePercentage } from \"../parsers\";\nimport { useInputField } from \"../input_field_hook\";\nimport { useNumpadDecimal } from \"../numpad_decimal_hook\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class PercentageField extends Component {\n    static template = \"web.PercentageField\";\n    static props = {\n        ...standardFieldProps,\n        digits: { type: Array, optional: true },\n        placeholder: { type: String, optional: true },\n    };\n\n    setup() {\n        useInputField({\n            getValue: () =>\n                formatPercentage(this.props.record.data[this.props.name], {\n                    digits: this.props.digits,\n                    noSymbol: true,\n                    field: this.props.record.fields[this.props.name],\n                }),\n            refName: \"numpadDecimal\",\n            parse: (v) => parsePercentage(v),\n        });\n        useNumpadDecimal();\n    }\n\n    get formattedValue() {\n        return formatPercentage(this.props.record.data[this.props.name], {\n            digits: this.props.digits,\n            field: this.props.record.fields[this.props.name],\n        });\n    }\n}\n\nexport const percentageField = {\n    component: PercentageField,\n    displayName: _t(\"Percentage\"),\n    supportedTypes: [\"integer\", \"float\"],\n    extractProps: ({ 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\n        return {\n            digits,\n            placeholder: attrs.placeholder,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"percentage\", percentageField);\n", "import { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useInputField } from \"../input_field_hook\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class PhoneField extends Component {\n    static template = \"web.PhoneField\";\n    static props = {\n        ...standardFieldProps,\n        placeholder: { type: String, optional: true },\n    };\n\n    setup() {\n        useInputField({ getValue: () => this.props.record.data[this.props.name] || \"\" });\n    }\n    get phoneHref() {\n        return \"tel:\" + this.props.record.data[this.props.name].replace(/\\s+/g, \"\");\n    }\n}\n\nexport const phoneField = {\n    component: PhoneField,\n    displayName: _t(\"Phone\"),\n    supportedTypes: [\"char\"],\n    extractProps: ({ attrs }) => ({\n        placeholder: attrs.placeholder,\n    }),\n};\n\nregistry.category(\"fields\").add(\"phone\", phoneField);\n\nclass FormPhoneField extends PhoneField {\n    static template = \"web.FormPhoneField\";\n}\n\nexport const formPhoneField = {\n    ...phoneField,\n    component: FormPhoneField,\n};\n\nregistry.category(\"fields\").add(\"form.phone\", formPhoneField);\n", "import { useCommand } from \"@web/core/commands/command_hook\";\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class PriorityField extends Component {\n    static template = \"web.PriorityField\";\n    static props = {\n        ...standardFieldProps,\n        withCommand: { type: Boolean, optional: true },\n        autosave: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        this.state = useState({\n            index: -1,\n        });\n        if (this.props.withCommand) {\n            for (const command of this.commands) {\n                useCommand(...command);\n            }\n        }\n    }\n\n    get commands() {\n        const commandName = _t(\"Set priority...\");\n        return [\n            [\n                commandName,\n                () => {\n                    return {\n                        placeholder: commandName,\n                        providers: [\n                            {\n                                provide: () =>\n                                    this.options.map((value) => ({\n                                        name: value[1],\n                                        action: () => {\n                                            this.updateRecord(value[0]);\n                                        },\n                                    })),\n                            },\n                        ],\n                    };\n                },\n                { category: \"smart_action\", hotkey: \"alt+r\" },\n            ],\n        ];\n    }\n\n    get tooltipLabel() {\n        return this.props.record.fields[this.props.name].string;\n    }\n    get options() {\n        return Array.from(this.props.record.fields[this.props.name].selection);\n    }\n    get index() {\n        return this.state.index > -1\n            ? this.state.index\n            : this.options.findIndex((o) => o[0] === this.props.record.data[this.props.name]);\n    }\n\n    getTooltip(value) {\n        return this.tooltipLabel && this.tooltipLabel !== value\n            ? `${this.tooltipLabel}: ${value}`\n            : value;\n    }\n    /**\n     * @param {string} value\n     */\n    onStarClicked(value) {\n        if (this.props.record.data[this.props.name] === value) {\n            this.state.index = -1;\n            this.updateRecord(this.options[0][0]);\n        } else {\n            this.updateRecord(value);\n        }\n    }\n\n    async updateRecord(value) {\n        await this.props.record.update({ [this.props.name]: value }, { save: this.props.autosave });\n    }\n}\n\nexport const priorityField = {\n    component: PriorityField,\n    displayName: _t(\"Priority\"),\n    supportedOptions: [\n        {\n            label: _t(\"Autosave\"),\n            name: \"autosave\",\n            type: \"boolean\",\n            default: true,\n            help: _t(\n                \"If checked, the record will be saved immediately when the field is modified.\"\n            ),\n        },\n    ],\n    supportedTypes: [\"selection\"],\n    extractProps({ options, viewType }, dynamicInfo) {\n        return {\n            withCommand: viewType === \"form\",\n            readonly: dynamicInfo.readonly,\n            autosave: \"autosave\" in options ? !!options.autosave : true,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"priority\", priorityField);\n", "import { registry } from \"@web/core/registry\";\nimport { progressBarField, ProgressBarField } from \"./progress_bar_field\";\n\nexport class KanbanProgressBarField extends ProgressBarField {\n    get isEditable() {\n        return this.props.isEditable;\n    }\n}\n\nexport const kanbanProgressBarField = {\n    ...progressBarField,\n    component: KanbanProgressBarField,\n};\n\nregistry.category(\"fields\").add(\"kanban.progressbar\", kanbanProgressBarField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useNumpadDecimal } from \"../numpad_decimal_hook\";\nimport { parseFloat } from \"../parsers\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component, useRef, useState } from \"@odoo/owl\";\nconst formatters = registry.category(\"formatters\");\n\nexport class ProgressBarField extends Component {\n    static template = \"web.ProgressBarField\";\n    static props = {\n        ...standardFieldProps,\n        maxValueField: { type: [String, Number], optional: true },\n        currentValueField: { type: String, optional: true },\n        isEditable: { type: Boolean, optional: true },\n        isCurrentValueEditable: { type: Boolean, optional: true },\n        isMaxValueEditable: { type: Boolean, optional: true },\n        title: { type: String, optional: true },\n        overflowClass: { type: String, optional: true },\n    };\n\n    setup() {\n        useNumpadDecimal();\n        this.root = useRef(\"numpadDecimal\");\n        this.maxValueRef = useRef(\"maxValue\");\n        this.currentValueRef = useRef(\"currentValue\");\n\n        const { currentValueField, maxValueField, name } = this.props;\n        this.currentValueField = currentValueField ? currentValueField : name;\n        if (maxValueField) {\n            this.maxValueField = maxValueField;\n        }\n\n        this.state = useState({\n            isEditing: false,\n        });\n    }\n\n    get isEditable() {\n        return this.props.isEditable && !this.props.readonly;\n    }\n    get isPercentage() {\n        return !this.props.maxValueField || !isNaN(this.props.maxValueField);\n    }\n\n    get currentValue() {\n        return this.props.record.data[this.currentValueField] || 0;\n    }\n\n    get maxValue() {\n        return this.props.record.data[this.maxValueField] || 100;\n    }\n\n    get progressBarColorClass() {\n        return this.currentValue > this.maxValue ? this.props.overflowClass : \"bg-primary\";\n    }\n\n    formatCurrentValue(humanReadable = !this.state.isEditing) {\n        const formatter = formatters.get(Number.isInteger(this.currentValue) ? \"integer\" : \"float\");\n        return formatter(this.currentValue, { humanReadable });\n    }\n    formatMaxValue(humanReadable = !this.state.isEditing) {\n        const formatter = formatters.get(Number.isInteger(this.maxValue) ? \"integer\" : \"float\");\n        return formatter(this.maxValue, { humanReadable });\n    }\n\n    onValueChange(value, fieldName) {\n        let parsedValue;\n        try {\n            parsedValue = parseFloat(value);\n        } catch {\n            this.props.record.setInvalidField(this.props.name);\n            return;\n        }\n\n        if (this.props.record.fields[fieldName].type === \"integer\") {\n            parsedValue = Math.floor(parsedValue);\n        }\n        this.props.record.update({ [fieldName]: parsedValue }, { save: this.props.readonly });\n    }\n    onCurrentValueChange(ev) {\n        this.onValueChange(ev.target.value, this.currentValueField);\n    }\n    onMaxValueChange(ev) {\n        this.onValueChange(ev.target.value, this.maxValueField);\n    }\n\n    onInputBlur() {\n        if (\n            document.activeElement !== this.maxValueRef.el &&\n            document.activeElement !== this.currentValueRef.el\n        ) {\n            this.state.isEditing = false;\n        }\n    }\n    onInputFocus() {\n        this.state.isEditing = true;\n    }\n}\n\nexport const progressBarField = {\n    component: ProgressBarField,\n    displayName: _t(\"Progress Bar\"),\n    supportedOptions: [\n        {\n            label: _t(\"Can edit value\"),\n            name: \"editable\",\n            type: \"boolean\",\n        },\n        {\n            label: _t(\"Can edit max value\"),\n            name: \"edit_max_value\",\n            type: \"boolean\",\n        },\n        {\n            label: _t(\"Current value field\"),\n            name: \"current_value\",\n            type: \"field\",\n            availableTypes: [\"integer\", \"float\"],\n            help: _t(\n                \"Use to override the display value (e.g. if your progress bar is a computed percentage but you want to display the actual field value instead).\"\n            ),\n        },\n        {\n            label: _t(\"Max value field\"),\n            name: \"max_value\",\n            type: \"field\",\n            availableTypes: [\"integer\", \"float\"],\n            help: _t(\n                \"Field that holds the maximum value of the progress bar. If set, will be displayed next to the progress bar (e.g. 10 / 200).\"\n            ),\n        },\n        {\n            label: _t(\"Overflow style\"),\n            name: \"overflow_class\",\n            type: \"string\",\n            availableTypes: [\"integer\", \"float\"],\n            help: _t(\n                \"Bootstrap classname to customize the style of the progress bar when the maximum value is exceeded\"\n            ),\n            default: \"bg-secondary\",\n        },\n    ],\n    supportedTypes: [\"integer\", \"float\"],\n    extractProps: ({ attrs, options }) => ({\n        maxValueField: options.max_value,\n        currentValueField: options.current_value,\n        isEditable: !options.readonly && options.editable,\n        isCurrentValueEditable: options.editable && !options.edit_max_value,\n        isMaxValueEditable: options.editable && options.edit_max_value,\n        title: attrs.title,\n        overflowClass: options.overflow_class || \"bg-secondary\",\n    }),\n};\n\nregistry.category(\"fields\").add(\"progressbar\", progressBarField);\n", "import { registry } from \"@web/core/registry\";\nimport { propertiesField, PropertiesField } from \"./properties_field\";\n\nexport class CardPropertiesField extends PropertiesField {\n    static template = \"web.CardPropertiesField\";\n\n    async _checkDefinitionAccess() {\n        // can not edit properties definition in cards\n        this.state.canChangeDefinition = false;\n    }\n}\n\nexport const cardPropertiesField = {\n    ...propertiesField,\n    component: CardPropertiesField,\n};\n\nregistry.category(\"fields\").add(\"calendar.properties\", cardPropertiesField);\nregistry.category(\"fields\").add(\"kanban.properties\", cardPropertiesField);\nregistry.category(\"fields\").add(\"hierarchy.properties\", cardPropertiesField);\n", "import { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { reposition } from \"@web/core/position/utils\";\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { pick } from \"@web/core/utils/objects\";\nimport { useSortable } from \"@web/core/utils/sortable_owl\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\nimport { uuid } from \"../../utils\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { PropertyDefinition } from \"./property_definition\";\nimport { PropertyValue } from \"./property_value\";\n\nimport { Component, onWillStart, useEffect, useRef, useState } from \"@odoo/owl\";\n\nexport class PropertiesField extends Component {\n    static template = \"web.PropertiesField\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n        PropertyDefinition,\n        PropertyValue,\n    };\n    static props = {\n        ...standardFieldProps,\n        context: { type: Object, optional: true },\n        columns: {\n            type: Number,\n            optional: true,\n            validate: (columns) => [1, 2].includes(columns),\n        },\n        showAddButton: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        this.notification = useService(\"notification\");\n        this.orm = useService(\"orm\");\n        this.dialogService = useService(\"dialog\");\n        this.popover = usePopover(PropertyDefinition, {\n            closeOnClickAway: this.checkPopoverClose,\n            popoverClass: \"o_property_field_popover\",\n            position: \"top\",\n            onClose: () => this.onCloseCurrentPopover?.(),\n            fixedPosition: true,\n            arrow: false,\n        });\n        this.propertiesRef = useRef(\"properties\");\n\n        let currentResId;\n        useRecordObserver((record) => {\n            if (currentResId !== record.resId) {\n                currentResId = record.resId;\n                this._saveInitialPropertiesValues();\n            }\n        });\n\n        const field = this.props.record.fields[this.props.name];\n        this.definitionRecordField = field.definition_record;\n\n        this.state = useState({\n            canChangeDefinition: true,\n            movedPropertyName: null,\n            showAddButton: this.props.showAddButton,\n            unfoldedSeparators: this._getUnfoldedSeparators(),\n        });\n\n        // Properties can be added from the cogmenu of the form controller\n        if (this.env.config?.viewType === \"form\") {\n            useBus(this.env.model.bus, \"PROPERTY_FIELD:ADD_PROPERTY_VALUE\", () => {\n                this.onPropertyCreate();\n            });\n        }\n\n        onWillStart(async () => {\n            await this._checkDefinitionAccess();\n        });\n\n        useEffect(\n            () => {\n                if (this.openPropertyDefinition) {\n                    const propertyName = this.openPropertyDefinition;\n                    const labels = this.propertiesRef.el.querySelectorAll(\n                        `.o_property_field[property-name=\"${propertyName}\"] .o_field_property_open_popover`\n                    );\n                    this.openPropertyDefinition = null;\n                    const lastLabel = labels[labels.length - 1];\n                    this._openPropertyDefinition(lastLabel, propertyName, true);\n                }\n            },\n            () => [this.openPropertyDefinition]\n        );\n\n        useEffect(() => this._movePopoverIfNeeded());\n\n        // sort properties\n        useSortable({\n            enable: () => !this.props.readonly && this.state.canChangeDefinition,\n            ref: this.propertiesRef,\n            handle: \".o_field_property_label .oi-draggable\",\n            // on mono-column layout, allow to move before a separator to make the usage more fluid\n            elements:\n                this.renderedColumnsCount === 1\n                    ? \"*:is(.o_property_field, .o_field_property_group_label)\"\n                    : \".o_property_field\",\n            groups: \".o_property_group\",\n            connectGroups: true,\n            cursor: \"grabbing\",\n            onDragStart: ({ element, group }) => {\n                this.propertiesRef.el.classList.add(\"o_property_dragging\");\n                element.classList.add(\"o_property_drag_item\");\n                group.classList.add(\"o_property_drag_group\");\n                // without this, if we edit a char property, move it,\n                // the change will be reset when we drop the property\n                document.activeElement.blur();\n            },\n            onDrop: async ({ parent, element, next, previous }) => {\n                const from = element.getAttribute(\"property-name\");\n                let to = previous && previous.getAttribute(\"property-name\");\n                let moveBefore = false;\n                if (!to && next) {\n                    // we move the element at the first position inside a group\n                    // or at the first position of a column\n                    if (next.classList.contains(\"o_field_property_group_label\")) {\n                        // mono-column layout, move before the separator\n                        next = next.closest(\".o_property_group\");\n                    }\n                    to = next.getAttribute(\"property-name\");\n                    moveBefore = !!to;\n                }\n                if (!to) {\n                    // we move in an empty group or outside of the DOM element\n                    // move the element at the end of the group\n                    const groupName = parent.getAttribute(\"property-name\");\n                    const group = this.groupedPropertiesList.find(\n                        (group) => group.name === groupName\n                    );\n                    if (!group) {\n                        to = null;\n                        moveBefore = false;\n                    } else {\n                        to = group.elements.length ? group.elements.at(-1).name : groupName;\n                    }\n                }\n                await this.onPropertyMoveTo(from, to, moveBefore);\n            },\n            onDragEnd: ({ element }) => {\n                this.propertiesRef.el.classList.remove(\"o_property_dragging\");\n                element.classList.remove(\"o_property_drag_item\");\n                const targetGroup = this.propertiesRef.el.querySelector(\".o_property_drag_group\");\n                if (targetGroup) {\n                    targetGroup.classList.remove(\"o_property_drag_group\");\n                }\n            },\n            onGroupEnter: ({ group }) => {\n                group.classList.add(\"o_property_drag_group\");\n                this._unfoldSeparators([group.getAttribute(\"property-name\")], true);\n            },\n            onGroupLeave: ({ group }) => {\n                group.classList.remove(\"o_property_drag_group\");\n            },\n        });\n\n        // sort group of properties\n        useSortable({\n            enable: () => !this.props.readonly && this.state.canChangeDefinition,\n            ref: this.propertiesRef,\n            handle: \".o_field_property_group_label .oi-draggable\",\n            elements: \".o_property_group:not([property-name=''])\",\n            cursor: \"grabbing\",\n            onDragStart: ({ element }) => {\n                this.propertiesRef.el.classList.add(\"o_property_dragging\");\n                element.classList.add(\"o_property_drag_item\");\n                document.activeElement.blur();\n            },\n            onDrop: async ({ element, previous }) => {\n                const from = element.getAttribute(\"property-name\");\n                const to = previous && previous.getAttribute(\"property-name\");\n                await this.onGroupMoveTo(from, to);\n            },\n            onDragEnd: ({ element }) => {\n                this.propertiesRef.el.classList.remove(\"o_property_dragging\");\n                element.classList.remove(\"o_property_drag_item\");\n            },\n        });\n    }\n\n    /* --------------------------------------------------------\n     * Public methods / Getters\n     * -------------------------------------------------------- */\n\n    /**\n     * Return the number of columns we have to render\n     * (The properties can be split in many column,\n     * to follow the layout of the form view)\n     *\n     * @returns {object}\n     */\n    get renderedColumnsCount() {\n        return this.env.isSmall ? 1 : this.props.columns;\n    }\n\n    /**\n     * Return the current properties value.\n     *\n     * Make a deep copy of this properties values, so when we will modify it\n     * in the events, we won't re-use same object (can lead to issue, e.g. if we\n     * discard a form view, we should be able to restore the old props).\n     *\n     * @returns {array}\n     */\n    get propertiesList() {\n        const propertiesValues = this.props.record.data[this.props.name] || [];\n        return propertiesValues.filter((definition) => !definition.definition_deleted);\n    }\n\n    /**\n     * Return the current properties value splitted in multiple groups/columns.\n     * Each properties are splitted in groups, thanks to the separators, and\n     * groups are splitted in columns (the columns property is the number of groups\n     * we have on a row).\n     *\n     * The groups are created with the separators (special type of property) so\n     * the order mater in the group creation.\n     *\n     * @returns {Array<Array>}\n     */\n    get groupedPropertiesList() {\n        const propertiesList = this.propertiesList;\n        // default invisible group\n        const groupedProperties =\n            propertiesList[0]?.type !== \"separator\"\n                ? [{ title: null, name: null, elements: [], invisibleLabel: true }]\n                : [];\n\n        propertiesList.forEach((property) => {\n            if (property.type === \"separator\") {\n                groupedProperties.push({\n                    title: property.string,\n                    name: property.name,\n                    elements: [],\n                });\n            } else {\n                groupedProperties.at(-1).elements.push(property);\n            }\n        });\n\n        if (groupedProperties.length === 1) {\n            // only one group, split this group in the columns to take the entire width\n            const invisibleLabel = propertiesList[0]?.type !== \"separator\";\n            groupedProperties[0].elements = [];\n            groupedProperties[0].invisibleLabel = invisibleLabel;\n            for (let col = 1; col < this.renderedColumnsCount; ++col) {\n                groupedProperties.push({\n                    title: null,\n                    name: groupedProperties[0].name,\n                    columnSeparator: true,\n                    elements: [],\n                    invisibleLabel,\n                });\n            }\n            const properties = propertiesList.filter((property) => property.type !== \"separator\");\n            properties.forEach((property, index) => {\n                const columnIndex = Math.floor(\n                    (index * this.renderedColumnsCount) / properties.length\n                );\n                groupedProperties[columnIndex].elements.push(property);\n            });\n        }\n\n        return groupedProperties;\n    }\n\n    /**\n     * Return the id of the definition record.\n     *\n     * @returns {integer}\n     */\n    get definitionRecordId() {\n        return this.props.record.data[this.definitionRecordField][0];\n    }\n\n    /**\n     * Return the model of the definition record.\n     *\n     * @returns {string}\n     */\n    get definitionRecordModel() {\n        return this.props.record.fields[this.definitionRecordField].relation;\n    }\n\n    /**\n     * Return true if we should close the popover containing the\n     * properties definition based on the target received.\n     *\n     * If we edit the datetime, it will open a popover with the date picker\n     * component, but this component won't be a child of the current popover.\n     * So when we will click on it to select a date, it will close the definition\n     * popover. It's the same for other similar components (many2one modal, etc).\n     *\n     * @param {HTMLElement} target\n     * @returns {boolean}\n     */\n    checkPopoverClose(target) {\n        if (target.closest(\".o_datetime_picker\")) {\n            // selected a datetime, do not close the definition popover\n            return false;\n        }\n\n        if (target.closest(\".modal\")) {\n            // close a many2one modal\n            return false;\n        }\n\n        if (target.closest(\".o_tag_popover\")) {\n            // tag color popover\n            return false;\n        }\n\n        if (target.closest(\".o_model_field_selector_popover\")) {\n            // domain selector\n            return false;\n        }\n\n        return true;\n    }\n\n    /**\n     * Generate an unique ID to be used in the DOM.\n     *\n     * @returns {string}\n     */\n    generateUniqueDomID() {\n        return `property_${uuid()}`;\n    }\n\n    /**\n     * Generate a new property name.\n     *\n     * @returns {string}\n     */\n    generatePropertyName() {\n        return uuid();\n    }\n\n    /* --------------------------------------------------------\n     * Event handlers\n     * -------------------------------------------------------- */\n\n    /**\n     * Move the given property up or down in the list.\n     *\n     * @param {string} propertyName\n     * @param {string} direction, either \"up\" or \"down\"\n     */\n    async onPropertyMove(propertyName, direction) {\n        const propertiesValues = this.propertiesList || [];\n        const propertyIndex = propertiesValues.findIndex(\n            (property) => property.name === propertyName\n        );\n\n        const targetIndex = propertyIndex + (direction === \"down\" ? 1 : -1);\n        if (targetIndex < 0 || targetIndex >= propertiesValues.length) {\n            this.notification.add(\n                direction === \"down\"\n                    ? _t(\"This field is already last\")\n                    : _t(\"This field is already first\"),\n                { type: \"warning\" }\n            );\n            return;\n        }\n        this.state.movedPropertyName = propertyName;\n\n        const prop = propertiesValues[targetIndex];\n        propertiesValues[targetIndex] = propertiesValues[propertyIndex];\n        propertiesValues[propertyIndex] = prop;\n        propertiesValues[propertyIndex].definition_changed = true;\n\n        this._unfoldPropertyGroup(targetIndex, propertiesValues);\n\n        await this.props.record.update({ [this.props.name]: propertiesValues });\n        // move the popover once the DOM is updated\n        this.movePopoverToProperty = propertyName;\n    }\n\n    /**\n     * Move a property after the target property.\n     *\n     * @param {string} propertyName\n     * @param {string} toPropertyName, the target property\n     *  (null if we move the property to the first index)\n     */\n    onPropertyMoveTo(propertyName, toPropertyName, moveBefore) {\n        const propertiesValues = this.propertiesList || [];\n\n        let fromIndex = propertiesValues.findIndex((property) => property.name === propertyName);\n        let toIndex = propertiesValues.findIndex((property) => property.name === toPropertyName);\n        const columnSize = Math.ceil(propertiesValues.length / this.renderedColumnsCount);\n\n        // if we have no separator at first, we might want to create some\n        // to keep the initial column separation (only if needed, if we move properties\n        // inside the same column we do nothing)\n        if (\n            this.renderedColumnsCount > 1 &&\n            !propertiesValues.some((p, index) => index !== 0 && p.type === \"separator\") &&\n            Math.floor(fromIndex / columnSize) !== Math.floor(toIndex / columnSize)\n        ) {\n            const newSeparators = [];\n            for (let col = 0; col < this.renderedColumnsCount; ++col) {\n                const separatorIndex = columnSize * col + newSeparators.length;\n\n                if (propertiesValues[separatorIndex]?.type === \"separator\") {\n                    newSeparators.push(propertiesValues[separatorIndex].name);\n                    continue;\n                }\n                const newSeparator = {\n                    type: \"separator\",\n                    string: _t(\"Group %s\", col + 1),\n                    name: this.generatePropertyName(),\n                };\n                newSeparators.push(newSeparator.name);\n                propertiesValues.splice(separatorIndex, 0, newSeparator);\n            }\n            this._unfoldSeparators(newSeparators, true);\n            toPropertyName = toPropertyName || propertiesValues.at(-1).name;\n\n            // indexes might have changed\n            fromIndex = propertiesValues.findIndex((property) => property.name === propertyName);\n            toIndex = propertiesValues.findIndex((property) => property.name === toPropertyName);\n        }\n\n        if (moveBefore) {\n            toIndex--;\n        }\n        if (toIndex < fromIndex) {\n            // the first splice operation will change the index\n            toIndex++;\n        }\n        propertiesValues.splice(toIndex, 0, propertiesValues.splice(fromIndex, 1)[0]);\n        propertiesValues[0].definition_changed = true;\n        this.props.record.update({ [this.props.name]: propertiesValues });\n    }\n\n    /**\n     * Move a group of properties after the target group.\n     *\n     * @param {string} propertyName\n     * @param {string} toPropertyName, the target group (separator)\n     *  (null if we move the group to the first index)\n     */\n    onGroupMoveTo(propertyName, toPropertyName) {\n        const propertiesValues = this.propertiesList || [];\n        const fromIndex = propertiesValues.findIndex((property) => property.name === propertyName);\n        const toIndex = propertiesValues.findIndex((property) => property.name === toPropertyName);\n        if (\n            propertiesValues[fromIndex].type !== \"separator\" ||\n            (toIndex >= 0 && propertiesValues[toIndex].type !== \"separator\")\n        ) {\n            throw new Error(\"Something went wrong\");\n        }\n\n        // find the next separator index\n        const getNextSeparatorIndex = (startIndex) => {\n            const nextSeparatorIndex = propertiesValues.findIndex(\n                (property, index) => property.type === \"separator\" && index > startIndex\n            );\n            return nextSeparatorIndex < 0 ? propertiesValues.length : nextSeparatorIndex;\n        };\n        const groupSize = getNextSeparatorIndex(fromIndex) - fromIndex;\n        let targetIndex = getNextSeparatorIndex(toIndex);\n        if (targetIndex > fromIndex) {\n            // the size of the array will change after the first splice\n            // so we need to correct the index\n            targetIndex -= groupSize;\n        }\n        propertiesValues.splice(targetIndex, 0, ...propertiesValues.splice(fromIndex, groupSize));\n        propertiesValues[0].definition_changed = true;\n        this.props.record.update({ [this.props.name]: propertiesValues });\n    }\n\n    /**\n     * The value / definition of the given property has been changed.\n     * `propertyValue` contains the definition of the property with the value.\n     *\n     * @param {string} propertyName\n     * @param {object} propertyValue\n     */\n    onPropertyValueChange(propertyName, propertyValue) {\n        const propertiesValues = this.propertiesList;\n        propertiesValues.find((property) => property.name === propertyName).value = propertyValue;\n        this.props.record.update({ [this.props.name]: propertiesValues });\n    }\n\n    /**\n     * Check if the definition is not already opened\n     * and if it's not the case, open the popover with the property definition.\n     *\n     * @param {event} event\n     * @param {string} propertyName\n     */\n    async onPropertyEdit(event, propertyName) {\n        event.stopPropagation();\n        event.preventDefault();\n        if (!(await this.checkDefinitionWriteAccess())) {\n            this.notification.add(\n                _t(\"You need edit access on the parent document to update these property fields\"),\n                { type: \"warning\" }\n            );\n            return;\n        }\n        if (event.target.classList.contains(\"disabled\")) {\n            // remove the glitch if we click on the edit button\n            // while the popover is already opened\n            return;\n        }\n\n        event.target.classList.add(\"disabled\");\n        this._openPropertyDefinition(event.target, propertyName, false);\n    }\n\n    /**\n     * The property definition or value has been changed.\n     *\n     * @param {object} propertyDefinition\n     */\n    async onPropertyDefinitionChange(propertyDefinition) {\n        propertyDefinition[\"definition_changed\"] = true;\n        if (propertyDefinition.type === \"separator\") {\n            // remove all other keys\n            propertyDefinition = pick(\n                propertyDefinition,\n                \"name\",\n                \"string\",\n                \"definition_changed\",\n                \"type\"\n            );\n        }\n        const propertiesValues = this.propertiesList;\n        const propertyIndex = this._getPropertyIndex(propertyDefinition.name);\n\n        const oldType = propertiesValues[propertyIndex].type;\n        const newType = propertyDefinition.type;\n\n        this._regeneratePropertyName(propertyDefinition);\n\n        propertiesValues[propertyIndex] = propertyDefinition;\n        await this.props.record.update({ [this.props.name]: propertiesValues });\n\n        if (newType === \"separator\" && oldType !== \"separator\") {\n            // unfold automatically the new separator\n            this._unfoldSeparators([propertyDefinition.name], true);\n            // layout has been changed, move the definition popover\n            this.movePopoverToProperty = propertyDefinition.name;\n        } else if (oldType === \"separator\" && newType !== \"separator\") {\n            // unfold automatically the previous separator\n            const previousSeperator = propertiesValues.findLast(\n                (property, index) => index < propertyIndex && property.type === \"separator\"\n            );\n            if (previousSeperator) {\n                this._unfoldSeparators([previousSeperator.name], true);\n            }\n            // layout has been changed, move the definition popover\n            this.movePopoverToProperty = propertyDefinition.name;\n        }\n    }\n\n    /**\n     * Mark a property as \"to delete\".\n     *\n     * @param {string} propertyName\n     */\n    onPropertyDelete(propertyName) {\n        this.popover.close();\n        const dialogProps = {\n            title: _t(\"Delete Property Field\"),\n            body: _t(\n                'Are you sure you want to delete this property field? It will be removed for everyone using the \"%(parentName)s\" %(parentFieldLabel)s.',\n                { parentName: this.parentName, parentFieldLabel: this.parentString }\n            ),\n            confirmLabel: _t(\"Delete\"),\n            confirm: () => {\n                const propertiesDefinitions = this.propertiesList;\n                propertiesDefinitions.find(\n                    (property) => property.name === propertyName\n                ).definition_deleted = true;\n                this.props.record.update({ [this.props.name]: propertiesDefinitions });\n            },\n            cancel: () => {},\n        };\n        this.dialogService.add(ConfirmationDialog, dialogProps);\n    }\n\n    async onPropertyCreate() {\n        if (!this.state.canChangeDefinition || !(await this.checkDefinitionWriteAccess())) {\n            this.notification.add(\n                _t(\"You need edit access on the parent document to update these property fields\"),\n                { type: \"warning\" }\n            );\n            return;\n        }\n        const propertiesDefinitions = this.propertiesList || [];\n\n        if (\n            propertiesDefinitions.length &&\n            propertiesDefinitions.some(\n                (prop) => prop.type !== \"separator\" && (!prop.string || !prop.string.length)\n            )\n        ) {\n            // do not allow to add new field until we set a label on the previous one\n            this.propertiesRef.el.closest(\".o_field_properties\").classList.add(\"o_field_invalid\");\n\n            this.notification.add(_t(\"Please complete your properties before adding a new one\"), {\n                type: \"warning\",\n            });\n            return;\n        }\n\n        this._unfoldPropertyGroup(propertiesDefinitions.length - 1, propertiesDefinitions);\n\n        this.propertiesRef.el.closest(\".o_field_properties\").classList.remove(\"o_field_invalid\");\n\n        const newName = this.generatePropertyName();\n        propertiesDefinitions.push({\n            name: newName,\n            string: _t(\"Property %s\", propertiesDefinitions.length + 1),\n            type: \"char\",\n            definition_changed: true,\n        });\n        this.openPropertyDefinition = newName;\n        this.state.showAddButton = true;\n        this.props.record.update({ [this.props.name]: propertiesDefinitions });\n    }\n\n    /**\n     * Fold / unfold the given separator property.\n     *\n     * @param {string} propertyName, Name of the separator property\n     * @param {boolean} forceUnfold, Always unfold\n     */\n    onSeparatorClick(propertyName) {\n        if (propertyName) {\n            this._unfoldSeparators([propertyName]);\n        }\n    }\n\n    /**\n     * Verify that we can write on properties, we can not change the definition\n     * if we don't have access for parent or if no parent is set.\n     */\n    async checkDefinitionWriteAccess() {\n        if (!this.definitionRecordId || !this.definitionRecordModel) {\n            return false;\n        }\n\n        return await user.checkAccessRight(\n            this.definitionRecordModel,\n            \"write\",\n            this.definitionRecordId\n        );\n    }\n\n    /**\n     * The tags list has been changed.\n     * If `newValue` is given, update the property value as well.\n     *\n     * @param {string} propertyName\n     * @param {array} newTags\n     * @param {array | null} newValue\n     */\n    onTagsChange(propertyName, newTags, newValue = null) {\n        const propertyDefinition = this.propertiesList.find(\n            (property) => property.name === propertyName\n        );\n        propertyDefinition.tags = newTags;\n        if (newValue !== null) {\n            propertyDefinition.value = newValue;\n        }\n        propertyDefinition.definition_changed = true;\n        this.onPropertyDefinitionChange(propertyDefinition);\n    }\n\n    /* --------------------------------------------------------\n     * Private methods\n     * -------------------------------------------------------- */\n\n    /**\n     * Generate the key to get the fold state from the local storage.\n     *\n     * @returns {string}\n     */\n    _getSeparatorFoldKey() {\n        const definitionRecordId = this.props.record.data[this.definitionRecordField][0];\n        const definitionRecordModel = this.props.record.fields[this.definitionRecordField].relation;\n        // store the fold / unfold information per definition record\n        // to clean the keys (to not keep information about removed separator)\n        return `properties.fold,${definitionRecordModel},${definitionRecordId}`;\n    }\n\n    /**\n     * Read the local storage and return the fold state stored in it.\n     *\n     * We clean the dictionary state because a property might have been deleted,\n     * and so there's no reason to keep the corresponding key in the dict.\n     *\n     * @returns {array} The folded state (name of the properties unfolded)\n     */\n    _getUnfoldedSeparators() {\n        const key = this._getSeparatorFoldKey();\n        const unfoldedSeparators = JSON.parse(window.localStorage.getItem(key)) || [];\n        const allPropertiesNames = this.propertiesList.map((property) => property.name);\n        // remove element that do not exist anymore (e.g. if we remove a separator)\n        return unfoldedSeparators.filter((name) => allPropertiesNames.includes(name));\n    }\n\n    /**\n     * Switch the folded state of the given separators.\n     *\n     * @param {array} separatorNames, list of separator name to fold / unfold\n     * @param {boolean} (forceUnfold) force the separator to be unfolded\n     */\n    _unfoldSeparators(separatorNames, forceUnfold) {\n        let unfoldedSeparators = this._getUnfoldedSeparators();\n        for (const separatorName of separatorNames) {\n            if (unfoldedSeparators.includes(separatorName)) {\n                if (!forceUnfold) {\n                    unfoldedSeparators = unfoldedSeparators.filter(\n                        (name) => name !== separatorName\n                    );\n                }\n            } else {\n                unfoldedSeparators.push(separatorName);\n            }\n        }\n        const key = this._getSeparatorFoldKey();\n        window.localStorage.setItem(key, JSON.stringify(unfoldedSeparators));\n        this.state.unfoldedSeparators = unfoldedSeparators;\n    }\n\n    /**\n     * Move the popover to the given property id.\n     * Used when we change the position of the properties.\n     *\n     * We change the popover position after the DOM has been updated (see @useEffect)\n     * because if we update it after changing the component properties,\n     */\n    _movePopoverIfNeeded() {\n        if (!this.movePopoverToProperty) {\n            return;\n        }\n        const propertyName = this.movePopoverToProperty;\n        this.movePopoverToProperty = null;\n\n        const popover = document\n            .querySelector(\".o_field_property_definition\")\n            .closest(\".o_popover\");\n        const target = document.querySelector(\n            `*[property-name=\"${propertyName}\"] .o_field_property_open_popover`\n        );\n\n        reposition(popover, target, { position: \"top\", margin: 10 });\n    }\n\n    /**\n     * Verify that we can write on the parent record,\n     * and therefor update the properties definition.\n     */\n    async _checkDefinitionAccess() {\n        this.parentName = this.props.record.data[this.definitionRecordField][1];\n        this.parentString = this.props.record.fields[this.definitionRecordField].string;\n\n        if (!this.definitionRecordModel) {\n            this.state.canChangeDefinition = false;\n            return;\n        }\n\n        // check if we can write on the definition record\n        this.state.canChangeDefinition = await user.checkAccessRight(\n            this.definitionRecordModel,\n            \"write\"\n        );\n    }\n\n    /**\n     * Regenerate a new name if needed or restore the original one.\n     * (see @_saveInitialPropertiesValues).\n     *\n     * If the type / model are the same, restore the original name to not reset the\n     * children otherwise, generate a new value so all value of the record are reset.\n     *\n     * @param {object} propertyDefinition\n     */\n    _regeneratePropertyName(propertyDefinition) {\n        const initialValues = this.initialValues[propertyDefinition.name];\n        if (\n            initialValues &&\n            propertyDefinition.type === initialValues.type &&\n            propertyDefinition.comodel === initialValues.comodel\n        ) {\n            // restore the original name\n            propertyDefinition.name = initialValues.name;\n        } else if (initialValues && initialValues.name === propertyDefinition.name) {\n            // Generate a new name to reset all values on other records.\n            // because the name has been changed on the definition,\n            // the old name on others record won't match the name on the definition\n            // and the python field will just ignore the old value.\n            // Store the new generated name to be able to restore it\n            // if needed.\n            const newName = this.generatePropertyName();\n            this.initialValues[newName] = initialValues;\n            propertyDefinition.name = newName;\n        }\n    }\n\n    /**\n     * Find the index of the given property in the list.\n     *\n     * Care about new name generation, if the name changed (because\n     * the type of the property, the model, etc changed), it will\n     * still find the index of the original property.\n     *\n     * @params {string} propertyName\n     * @returns {integer}\n     */\n    _getPropertyIndex(propertyName) {\n        const initialName = this.initialValues[propertyName]?.name || propertyName;\n        return this.propertiesList.findIndex((property) =>\n            [propertyName, initialName].includes(property.name)\n        );\n    }\n\n    /**\n     * If we change the type / model of a property, we will regenerate it's name\n     * (like if it was a new property) in order to reset the value of the children.\n     *\n     * But if we reset the old model / type, we want to be able to discard this\n     * modification (even if we save) and restore the original name.\n     *\n     * For that purpose, we save the original properties values.\n     */\n    _saveInitialPropertiesValues() {\n        // initial properties values, if the type or the model changed, the\n        // name will be regenerated in order to reset the value on the children\n        this.initialValues = {};\n        for (const propertiesValues of this.props.record.data[this.props.name] || []) {\n            this.initialValues[propertiesValues.name] = {\n                name: propertiesValues.name,\n                type: propertiesValues.type,\n                comodel: propertiesValues.comodel,\n            };\n        }\n    }\n\n    /**\n     * Open the popover with the property definition.\n     *\n     * @param {DomElement} target\n     * @param {string} propertyName\n     * @param {boolean} isNewlyCreated\n     */\n    _openPropertyDefinition(target, propertyName, isNewlyCreated = false) {\n        const propertiesList = this.propertiesList;\n        const propertyIndex = propertiesList.findIndex(\n            (property) => property.name === propertyName\n        );\n\n        // maybe the property has been renamed because the type / model\n        // changed, retrieve the new one\n        const currentName = (propertyName) => {\n            const propertiesList = this.propertiesList;\n            for (const [newName, initialValue] of Object.entries(this.initialValues)) {\n                if (initialValue.name === propertyName) {\n                    const prop = propertiesList.find((prop) => prop.name === newName);\n                    if (prop) {\n                        return newName;\n                    }\n                }\n            }\n            return propertyName;\n        };\n\n        this.onCloseCurrentPopover = () => {\n            this.onCloseCurrentPopover = null;\n            this.state.movedPropertyName = null;\n            target.classList.remove(\"disabled\");\n            if (isNewlyCreated) {\n                this._setDefaultPropertyValue(currentName(propertyName));\n            }\n        };\n\n        this.popover.open(target, {\n            readonly: this.props.readonly || !this.state.canChangeDefinition,\n            canChangeDefinition: this.state.canChangeDefinition,\n            checkDefinitionWriteAccess: () => this.checkDefinitionWriteAccess(),\n            propertyDefinition: this.propertiesList.find(\n                (property) => property.name === currentName(propertyName)\n            ),\n            context: this.props.context,\n            onChange: this.onPropertyDefinitionChange.bind(this),\n            onDelete: () => this.onPropertyDelete(currentName(propertyName)),\n            onPropertyMove: (direction) =>\n                this.onPropertyMove(currentName(propertyName), direction),\n            isNewlyCreated: isNewlyCreated,\n            propertyIndex: propertyIndex,\n            propertiesSize: propertiesList.length,\n        });\n    }\n\n    /**\n     * Write the default value on the given property.\n     *\n     * @param {string} propertyName\n     */\n    _setDefaultPropertyValue(propertyName) {\n        const propertiesValues = this.propertiesList;\n        const newProperty = propertiesValues.find((property) => property.name === propertyName);\n        newProperty.value = newProperty.default;\n        // it won't update the props, it's a trick because the onClose event of the popover\n        // is called not synchronously, and so if we click on \"create a property\", it will close\n        // the popover, calling this function, but the value will be overwritten because of onPropertyCreate\n        this.props.value = propertiesValues;\n        this.props.record.update({ [this.props.name]: propertiesValues });\n    }\n\n    /**\n     * Unfold the group of the given property.\n     *\n     * @param {integer} targetIndex\n     * @param {object} propertiesValues\n     */\n    _unfoldPropertyGroup(targetIndex, propertiesValues) {\n        const separator = propertiesValues.findLast(\n            (property, index) => property.type === \"separator\" && index <= targetIndex\n        );\n        if (separator) {\n            this._unfoldSeparators([separator.name], true);\n        }\n    }\n}\n\nexport const propertiesField = {\n    component: PropertiesField,\n    displayName: _t(\"Properties\"),\n    supportedTypes: [\"properties\"],\n    extractProps({ attrs }, dynamicInfo) {\n        return {\n            context: dynamicInfo.context,\n            columns: parseInt(attrs.columns || \"1\"),\n            showAddButton: exprToBoolean(attrs.showAddButton),\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"properties\", propertiesField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { PropertyValue } from \"./property_value\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { DomainSelector } from \"@web/core/domain_selector/domain_selector\";\nimport { Domain } from \"@web/core/domain\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { ModelSelector } from \"@web/core/model_selector/model_selector\";\nimport { Many2XAutocomplete } from \"@web/views/fields/relational_utils\";\nimport { useService, useOwnedDialogs } from \"@web/core/utils/hooks\";\nimport { PropertyDefinitionSelection } from \"./property_definition_selection\";\nimport { PropertyTags } from \"./property_tags\";\nimport { SelectCreateDialog } from \"@web/views/view_dialogs/select_create_dialog\";\nimport { uuid } from \"../../utils\";\n\nimport { Component, useState, onWillUpdateProps, useEffect, useRef } from \"@odoo/owl\";\n\nexport class PropertyDefinition extends Component {\n    static template = \"web.PropertyDefinition\";\n    static components = {\n        CheckBox,\n        DomainSelector,\n        Dropdown,\n        DropdownItem,\n        PropertyValue,\n        Many2XAutocomplete,\n        ModelSelector,\n        PropertyDefinitionSelection,\n        PropertyTags,\n    };\n    static props = {\n        readonly: { type: Boolean, optional: true },\n        canChangeDefinition: { type: Boolean, optional: true },\n        checkDefinitionWriteAccess: { type: Function, optional: true },\n        propertyDefinition: { optional: true },\n        context: { type: Object },\n        isNewlyCreated: { type: Boolean, optional: true },\n        // index and number of properties, to hide the move arrows when needed\n        propertyIndex: { type: Number },\n        propertiesSize: { type: Number },\n        // events\n        onChange: { type: Function, optional: true },\n        onDelete: { type: Function, optional: true },\n        onPropertyMove: { type: Function, optional: true },\n        // prop needed by the popover service\n        close: { type: Function, optional: true },\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n\n        this.propertyDefinitionRef = useRef(\"propertyDefinition\");\n        this.addDialog = useOwnedDialogs();\n\n        const defaultDefinition = {\n            name: false,\n            string: \"\",\n            type: \"char\",\n            default: \"\",\n        };\n        const propertyDefinition = {\n            ...defaultDefinition,\n            ...this.props.propertyDefinition,\n        };\n\n        this.state = useState({\n            propertyDefinition: propertyDefinition,\n            typeLabel: this._typeLabel(propertyDefinition.type),\n            resModel: \"\",\n            resModelDescription: \"\",\n            matchingRecordsCount: undefined,\n            propertyIndex: this.props.propertyIndex,\n        });\n\n        this._syncStateWithProps(propertyDefinition);\n\n        this._domInputIdPrefix = uuid();\n\n        // update the state and fetch needed information\n        onWillUpdateProps((newProps) => this._syncStateWithProps(newProps.value));\n\n        useEffect((event) => {\n            // focus the property label, when we open the property definition\n            if (this.labelFocused) {\n                // focus it only once\n                return;\n            }\n            this.labelFocused = true;\n            const labelInput = this.propertyDefinitionRef.el.querySelectorAll(\"input\")[0];\n            if (labelInput) {\n                if (this.props.isNewlyCreated) {\n                    labelInput.select();\n                } else {\n                    labelInput.focus();\n                }\n            }\n        });\n    }\n\n    /* --------------------------------------------------------\n     * Public methods / Getters\n     * -------------------------------------------------------- */\n\n    /**\n     * Return the list of property types with their labels.\n     *\n     * @returns {array}\n     */\n    get availablePropertyTypes() {\n        return [\n            [\"char\", _t(\"Text\")],\n            [\"boolean\", _t(\"Checkbox\")],\n            [\"integer\", _t(\"Integer\")],\n            [\"float\", _t(\"Decimal\")],\n            [\"date\", _t(\"Date\")],\n            [\"datetime\", _t(\"Date & Time\")],\n            [\"selection\", _t(\"Selection\")],\n            [\"tags\", _t(\"Tags\")],\n            [\"many2one\", _t(\"Many2one\")],\n            [\"many2many\", _t(\"Many2many\")],\n            [\"separator\", _t(\"Separator\")],\n        ];\n    }\n\n    /**\n     * Return True if the current properties is the first one in the list.\n     */\n    get isFirst() {\n        return this.state.propertyIndex === 0;\n    }\n\n    /**\n     * Return True if the current properties is the last one in the list.\n     */\n    get isLast() {\n        return this.state.propertyIndex === this.props.propertiesSize - 1;\n    }\n\n    /**\n     * Return the list of tag values, that will be selected by the PropertyTags\n     * component (all existing tags because we are editing the definition).\n     *\n     * @returns {array}\n     */\n    get propertyTagValues() {\n        return (this.state.propertyDefinition.tags || []).map((tag) => tag[0]);\n    }\n\n    /**\n     * Return an unique ID to be used in the DOM.\n     *\n     * @returns {string}\n     */\n    getUniqueDomID(suffix) {\n        return `property_definition_${this._domInputIdPrefix}_${suffix}`;\n    }\n\n    /* --------------------------------------------------------\n     * Event handlers\n     * -------------------------------------------------------- */\n\n    /**\n     * We changed the string of the property.\n     *\n     * @param {event} event\n     */\n    onPropertyLabelChange(event) {\n        const newString = event.target.value;\n        const propertyDefinition = {\n            ...this.state.propertyDefinition,\n            string: newString,\n        };\n        this.props.onChange(propertyDefinition);\n        this.state.propertyDefinition = propertyDefinition;\n    }\n\n    /**\n     * Pressed enter on the property label close the definition.\n     *\n     * @param {event} event\n     */\n    onPropertyLabelKeypress(event) {\n        if (event.key !== \"Enter\") {\n            return;\n        }\n        this.props.close();\n    }\n\n    /**\n     * We changed the default value of the property.\n     *\n     * @param {object} newDefault\n     */\n    onDefaultChange(newDefault) {\n        const propertyDefinition = {\n            ...this.state.propertyDefinition,\n            default: newDefault,\n        };\n        this.props.onChange(propertyDefinition);\n        this.state.propertyDefinition = propertyDefinition;\n    }\n\n    /**\n     * We selected a new property type.\n     *\n     * @param {string} newType\n     */\n    onPropertyTypeChange(newType) {\n        const propertyDefinition = {\n            ...this.state.propertyDefinition,\n            type: newType,\n        };\n        if ([\"integer\", \"float\"].includes(newType)) {\n            propertyDefinition.value = 0;\n            propertyDefinition.default = 0;\n        } else {\n            propertyDefinition.value = false;\n            propertyDefinition.default = false;\n        }\n\n        delete propertyDefinition.comodel;\n\n        this.props.onChange(propertyDefinition);\n        this.state.propertyDefinition = propertyDefinition;\n        this.state.resModel = \"\";\n        this.state.resModelDescription = \"\";\n        this.state.typeLabel = this._typeLabel(newType);\n    }\n\n    /**\n     * The model of the relational property (many2one / many2many) has been changed.\n     *\n     * @param {string} newModel\n     */\n    async onModelChange(newModel) {\n        const { label, technical } = newModel;\n\n        // if we change the model, we should reset the default value and the domain\n        const modelChanged = technical !== this.state.resModel;\n\n        this.state.resModel = technical;\n        this.state.resModelDescription = label;\n\n        const propertyDefinition = {\n            ...this.state.propertyDefinition,\n            comodel: technical,\n            default: modelChanged ? false : this.state.propertyDefinition.default,\n            value: modelChanged ? false : this.state.propertyDefinition.value,\n            domain: modelChanged ? false : this.state.propertyDefinition.domain,\n        };\n        this.props.onChange(propertyDefinition);\n        this.state.propertyDefinition = propertyDefinition;\n        await this._updateMatchingRecordsCount();\n    }\n\n    /**\n     * The domain of the relational property has been changed.\n     *\n     * @param {string} newDomain\n     */\n    async onDomainChange(newDomain) {\n        const propertyDefinition = {\n            ...this.state.propertyDefinition,\n            domain: newDomain,\n            default: false,\n        };\n        this.props.onChange(propertyDefinition);\n        this.state.propertyDefinition = propertyDefinition;\n        await this._updateMatchingRecordsCount();\n    }\n\n    /**\n     * Open the list view of the records matching the current domain.\n     */\n    onButtonDomainClick() {\n        this.addDialog(SelectCreateDialog, {\n            title: _t(\"Selected records\"),\n            noCreate: true,\n            multiSelect: false,\n            resModel: this.state.propertyDefinition.comodel,\n            domain: new Domain(this.state.propertyDefinition.domain || \"[]\").toList(),\n            context: this.props.context || {},\n        });\n    }\n\n    /**\n     * Move the current property up or down.\n     *\n     * @param {string} direction, either 'up' or 'down'\n     */\n    onPropertyMove(direction) {\n        if (direction === \"up\") {\n            this.state.propertyIndex--;\n        } else {\n            this.state.propertyIndex++;\n        }\n        this.props.onPropertyMove(direction);\n    }\n\n    /**\n     * We renamed / created / removed a selection option.\n     *\n     * @param {array} newOptions\n     */\n    onSelectionOptionChange(newOptions) {\n        const propertyDefinition = {\n            ...this.state.propertyDefinition,\n            selection: newOptions,\n        };\n        this.props.onChange(propertyDefinition);\n        this.state.propertyDefinition = propertyDefinition;\n    }\n\n    /**\n     * We renamed / created / removed tags.\n     *\n     * @param {array} newTags\n     */\n    onTagsChange(newTags) {\n        const propertyDefinition = {\n            ...this.state.propertyDefinition,\n            tags: newTags,\n        };\n        this.props.onChange(propertyDefinition);\n        this.state.propertyDefinition = propertyDefinition;\n    }\n\n    /**\n     * We activate / deactivate the property in the kanban view.\n     *\n     * @param {boolean} newValue\n     */\n    onViewInKanbanChange(newValue) {\n        const propertyDefinition = {\n            ...this.state.propertyDefinition,\n            view_in_cards: newValue,\n        };\n        this.props.onChange(propertyDefinition);\n        this.state.propertyDefinition = propertyDefinition;\n    }\n\n    /* --------------------------------------------------------\n     * Private methods\n     * -------------------------------------------------------- */\n\n    /**\n     * The property value changed (e.g. we discard a form view editing).\n     * Re-update the state with the new props.\n     *\n     * @param {object} propertyDefinition\n     */\n    async _syncStateWithProps(propertyDefinition) {\n        const newModel = propertyDefinition.comodel;\n        const currentModel = this.state.resModel;\n\n        this.state.propertyDefinition = propertyDefinition;\n        this.state.resModel = propertyDefinition.comodel;\n        this.state.typeLabel = this._typeLabel(propertyDefinition.type);\n        this.state.resModel = newModel;\n\n        if (newModel && newModel !== currentModel) {\n            // retrieve the model id and the model description from it's name\n            // \"res.partner\" => (5, \"Contact\")\n            try {\n                const result = await this.orm.call(\"ir.model\", \"display_name_for\", [[newModel]]);\n                if (!result || !result.length) {\n                    return;\n                }\n                this.state.resModelDescription = result[0].display_name;\n            } catch {\n                // can not read the ir.model\n                this.state.resModelDescription = _t(\n                    'You do not have access to the model \"%s\".',\n                    newModel\n                );\n            }\n\n            await this._updateMatchingRecordsCount();\n        } else if (!newModel) {\n            this.state.resModelDescription = \"\";\n        }\n    }\n\n    /**\n     * Update the number of records that match the current domain.\n     */\n    async _updateMatchingRecordsCount() {\n        if (this.state.resModel && this.state.resModel.length) {\n            const domainList = new Domain(this.state.propertyDefinition.domain || \"[]\").toList();\n\n            const result = await this.orm.call(\n                this.state.propertyDefinition.comodel,\n                \"search_count\",\n                [domainList]\n            );\n\n            this.state.matchingRecordsCount = result;\n        } else {\n            this.state.matchingRecordsCount = undefined;\n        }\n    }\n\n    /**\n     * Return the property label corresponding to the property type.\n     *\n     * @param {string} propertyType\n     * @returns {string}\n     */\n    _typeLabel(propertyType) {\n        const allTypes = this.availablePropertyTypes;\n        return allTypes.find((type) => type[0] === propertyType)[1];\n    }\n}\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { uuid } from \"../../utils\";\n\nimport { Component, useState, useRef, useEffect } from \"@odoo/owl\";\nimport { useSortable } from \"@web/core/utils/sortable_owl\";\n\nexport class PropertyDefinitionSelection extends Component {\n    static template = \"web.PropertyDefinitionSelection\";\n    static props = {\n        default: { type: String, optional: true },\n        options: {},\n        readonly: { type: Boolean, optional: true },\n        canChangeDefinition: { type: Boolean, optional: true },\n        onOptionsChange: { type: Function, optional: true }, // we add / remove / rename an option\n        onDefaultOptionChange: { type: Function, optional: true }, // we select a default value\n    };\n\n    setup() {\n        this.notification = useService(\"notification\");\n\n        // when we create a new option, it's added in the state\n        // when we have finished to edit it (blur / enter) we propagate\n        // the new value in the props\n        this.state = useState({\n            newOption: null,\n        });\n\n        this.propertyDefinitionSelectionRef = useRef(\"propertyDefinitionSelection\");\n        this.addButtonRef = useRef(\"addButton\");\n\n        useEffect(() => {\n            // automatically give the focus to the new option if it is empty\n            if (!this.state.newOption) {\n                return;\n            }\n            const inputs = this.propertyDefinitionSelectionRef.el.querySelectorAll(\n                \".o_field_property_selection_option input\"\n            );\n            if (inputs && inputs.length && !inputs[this.state.newOption.index].value) {\n                inputs[this.state.newOption.index].focus();\n            }\n        });\n\n        useSortable({\n            enable: () => this.props.canChangeDefinition && !this.props.readonly,\n            ref: this.propertyDefinitionSelectionRef,\n            handle: \".o_field_property_selection_drag\",\n            elements: \".o_field_property_selection_option\",\n            cursor: \"grabbing\",\n            onDrop: async ({ element, previous }) => {\n                const movedOption = element.getAttribute(\"option-name\");\n                const destinationOption = previous && previous.getAttribute(\"option-name\");\n                await this.onOptionMoveTo(movedOption, destinationOption);\n            },\n        });\n    }\n\n    /* --------------------------------------------------------\n     * Public methods / Getters\n     * -------------------------------------------------------- */\n\n    /**\n     * Return the current available options.\n     *\n     * Make a deep copy to not change original object to be able to restore\n     * the old props if we discard the editing of the forma view.\n     *\n     * @returns {array}\n     */\n    get options() {\n        return JSON.parse(JSON.stringify(this.props.options || []));\n    }\n\n    /**\n     * Options visible by the UI, include the newly created option if needed.\n     *\n     * @returns {array}\n     */\n    get optionsVisible() {\n        const options = this.options || [];\n        const newOption = this.state.newOption;\n        if (newOption) {\n            options.splice(newOption.index, 0, [newOption.name, \"\"]);\n        }\n        return options;\n    }\n\n    /* --------------------------------------------------------\n     * Event handlers\n     * -------------------------------------------------------- */\n\n    /**\n     * Add a new empty selection option.\n     */\n    onOptionCreate(index) {\n        this.state.newOption = {\n            index: index,\n            name: uuid(),\n        };\n    }\n\n    /**\n     * We changed an option label.\n     *\n     * @param {event} event\n     * @param {integer} optionIndex\n     */\n    onOptionChange(event, optionIndex) {\n        const target = event.target;\n        const newLabel = target.value;\n\n        if (this.options[optionIndex] && this.options[optionIndex][1] === newLabel) {\n            // do not update the props if we are already up to date\n            // e.g. we pressed enter already and lost focus\n            return;\n        }\n\n        const options = this.optionsVisible;\n\n        if (!newLabel || !newLabel.length) {\n            // if the label is empty, remove the option\n            options.splice(optionIndex, 1);\n        } else {\n            options[optionIndex][1] = newLabel;\n        }\n\n        const nonEmptyOptions = options.filter((option) => option[1] && option[1].length);\n        this.props.onOptionsChange(nonEmptyOptions);\n\n        if (this.state.newOption) {\n            // the new option has been propagated in the props\n            this.state.newOption = null;\n        }\n    }\n\n    /**\n     * Loose focus on an option, should cancel the newly\n     * created option if we didn't write on it.\n     *\n     * The attribute `_ignoreBlur` can be set if we don't want to remove\n     * the option if it's empty (and it will re-gain the focus at the\n     * next `useEffect` call).\n     *\n     * @param {event} event\n     * @param {integer} optionIndex\n     */\n    onOptionBlur(event, optionIndex) {\n        if (event.target.value && event.target.value.length) {\n            // losing the focus on an non-empty option should have no effect\n            return;\n        } else if (this._ignoreBlur) {\n            this._ignoreBlur = false;\n            return;\n        }\n\n        if (event.relatedTarget === this.addButtonRef.el) {\n            // lost the focus because we click on the add button\n            // if the value is empty, just ignore and cancel the event\n            event.stopPropagation();\n            event.preventDefault();\n        } else if (optionIndex === this.state.newOption?.index) {\n            // we remove the focus from the new empty option, remove it\n            this.state.newOption = null;\n        }\n    }\n\n    /**\n     * We pressed Enter on an option, add it if it's not\n     * empty and automatically create a new one.\n     *\n     * Navigate using the up / down arrows.\n     *\n     * @param {event} event\n     * @param {integer} optionIndex\n     */\n    onOptionKeyDown(event, optionIndex) {\n        if (event.key === \"Enter\") {\n            const newLabel = event.target.value;\n\n            if (!newLabel || !newLabel.length) {\n                // press enter on an empty option, just ignore it, nothing to save\n                event.stopPropagation();\n                event.preventDefault();\n                return;\n            }\n\n            this.onOptionChange(event, optionIndex);\n            this.onOptionCreate(optionIndex + 1);\n        } else if ([\"ArrowUp\", \"ArrowDown\"].includes(event.key)) {\n            event.stopPropagation();\n            event.preventDefault();\n\n            if (event.key === \"ArrowUp\" && optionIndex > 0) {\n                const previousInput = event.target\n                    .closest(\".o_field_property_selection_option\")\n                    .previousElementSibling.querySelector(\"input\");\n                previousInput.focus();\n            } else if (event.key === \"ArrowDown\" && optionIndex < this.optionsVisible.length - 1) {\n                const nextInput = event.target\n                    .closest(\".o_field_property_selection_option\")\n                    .nextElementSibling.querySelector(\"input\");\n                nextInput.focus();\n            }\n        }\n    }\n\n    /**\n     * Change the default selection option.\n     *\n     * @param {integer} optionIndex\n     */\n    onOptionSetDefault(optionIndex) {\n        if (!this.props.canChangeDefinition) {\n            return;\n        }\n        const newValue = this.optionsVisible[optionIndex][0];\n        this.props.onDefaultOptionChange(newValue !== this.props.default ? newValue : false);\n    }\n\n    /**\n     * Ask to remove the selection option.\n     *\n     * @param {integer} optionIndex\n     */\n    onOptionDelete(optionIndex) {\n        const options = this.optionsVisible;\n        options.splice(optionIndex, 1);\n        this.props.onOptionsChange(options);\n    }\n\n    /**\n     * Move an option after an other one.\n     *\n     * @param {string} from, the option to move\n     * @param {string} to, the target option\n     *      (null if we move the option at the first index)\n     */\n    onOptionMoveTo(movedOption, destinationOption) {\n        this._ignoreBlur = true;\n\n        let options = this.optionsVisible;\n        // if destinationOption is null, destinationOptionIndex will be -1 which is intended\n        let destinationOptionIndex = options.findIndex((option) => option[0] == destinationOption);\n        const movedOptionIndex = options.findIndex((option) => option[0] == movedOption);\n        if (destinationOptionIndex < movedOptionIndex) {\n            // the first splice operation won't change the index (and we except it to decrease it)\n            // for example if we have [A, B, C], and we move C such that it becomes [A, C, B]\n            // destinationOption is A and the destination index is 0, but we need the index to be 1\n            // (if the destination is after the moved option, the first splice will fix it for us)\n            destinationOptionIndex++;\n        }\n\n        const activeEl = document.activeElement;\n        if (\n            activeEl &&\n            this.propertyDefinitionSelectionRef.el.contains(activeEl) &&\n            activeEl.tagName === \"INPUT\"\n        ) {\n            const optionName = activeEl\n                .closest(\".o_field_property_selection_option\")\n                .getAttribute(\"option-name\");\n            const editedOptionIndex = options.findIndex((option) => option[0] === optionName);\n            // we might be editing the value and drag and drop something else just after\n            options[editedOptionIndex][1] = activeEl.value;\n        }\n\n        options.splice(destinationOptionIndex, 0, options.splice(movedOptionIndex, 1)[0]);\n\n        if (this.state.newOption) {\n            const newOptionIndex = options.findIndex(\n                (option) => option[0] === this.state.newOption.name\n            );\n            if (!options[newOptionIndex][1]?.length) {\n                // if there's an empty option, fix it's index in the state\n                // and do not propagate it in the props\n                this.state.newOption = {\n                    ...this.state.newOption,\n                    index: newOptionIndex,\n                };\n                options = options.filter((option) => option[0] !== this.state.newOption.name);\n            } else {\n                this.state.newOption = null;\n            }\n        }\n\n        this.props.onOptionsChange(options);\n    }\n}\n", "import { AutoComplete } from \"@web/core/autocomplete/autocomplete\";\nimport { ColorList } from \"@web/core/colorlist/colorlist\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { TagsList } from \"@web/core/tags_list/tags_list\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nimport { Component } from \"@odoo/owl\";\n\nclass PropertyTagsColorListPopover extends Component {\n    static template = \"web.PropertyTagsColorListPopover\";\n    static components = {\n        ColorList,\n    };\n    static props = {\n        colors: Array,\n        tag: Object,\n        switchTagColor: Function,\n        close: Function,\n    };\n}\n\n// property tags does not really need timeout because it does not make RPC calls\nexport class PropertyTagAutoComplete extends AutoComplete {}\nObject.assign(PropertyTagAutoComplete, { timeout: 0 });\n\nexport class PropertyTags extends Component {\n    static template = \"web.PropertyTags\";\n    static components = {\n        AutoComplete: PropertyTagAutoComplete,\n        TagsList,\n        ColorList,\n        Popover: PropertyTagsColorListPopover,\n    };\n\n    static props = {\n        id: { type: String, optional: true },\n        selectedTags: {}, // Tags value visible in the tags list\n        tags: {}, // Tags definition visible in the dropdown\n        // Define the behavior of the delete button on the tags, either\n        // \"value\" or \"tags\". If \"value\", the delete button will unselect\n        // the value, if \"tags\" the value will be removed from the definition.\n        deleteAction: { type: String },\n        readonly: { type: Boolean, optional: true },\n        canChangeTags: { type: Boolean, optional: true },\n        checkDefinitionWriteAccess: { type: Function, optional: true },\n        // Select a new value\n        onValueChange: { type: Function, optional: true },\n        // Change the tags definition (can also receive a second\n        // argument to update the current selected value)\n        onTagsChange: { type: Function, optional: true },\n    };\n    setup() {\n        this.notification = useService(\"notification\");\n        this.popover = usePopover(this.constructor.components.Popover);\n    }\n\n    /* --------------------------------------------------------\n     * Public methods / Getters\n     * -------------------------------------------------------- */\n\n    /**\n     * Return true if we should display the badges or just the tag label.\n     *\n     * @returns {array}\n     */\n    get displayBadge() {\n        return !this.env.config || this.env.config.viewType !== \"kanban\";\n    }\n\n    /**\n     * Return the list containing tags values and actions for the TagsList component.\n     *\n     * @returns {array}\n     */\n    get tagListItems() {\n        if (!this.props.selectedTags || !this.props.selectedTags.length) {\n            return [];\n        }\n\n        // Retrieve the tags label and color\n        // ['a', 'b'] =>  [['a', 'A', 5], ['b', 'B', 6]]\n        let value = this.props.tags.filter((tag) => this.props.selectedTags.indexOf(tag[0]) >= 0);\n\n        if (!this.displayBadge) {\n            // in kanban view e.g. to not show tag without color\n            value = value.filter((tag) => tag[2]);\n        }\n\n        const canDeleteTag =\n            !this.props.readonly &&\n            (this.props.canChangeTags || this.props.deleteAction === \"value\");\n\n        return value.map((tag) => {\n            const [tagId, tagLabel, tagColorIndex] = tag;\n            return {\n                id: tagId,\n                text: tagLabel,\n                className: this.props.canChangeTags ? \"\" : \"pe-none\",\n                colorIndex: tagColorIndex || 0,\n                onClick: (event) => this.onTagClick(event, tagId, tagColorIndex),\n                onDelete: canDeleteTag && (() => this.onTagDelete(tagId)),\n            };\n        });\n    }\n\n    /**\n     * Return the current selected tags.\n     * Make a deep copy to not make change on the original object\n     * and to be able to discard change.\n     *\n     * @returns {array}\n     */\n    get selectedTags() {\n        return JSON.parse(JSON.stringify(this.props.selectedTags || []));\n    }\n\n    /**\n     * Return the current tags that can be selected.\n     * Make a deep copy to not make change on the original object\n     * and to be able to discard change.\n     *\n     * @returns {array}\n     */\n    get availableTags() {\n        return JSON.parse(JSON.stringify(this.props.tags || []));\n    }\n\n    /**\n     * Options available in the autocomplete component.\n     *\n     * @returns {array}\n     */\n    get autocompleteSources() {\n        return [\n            {\n                options: (request) => {\n                    const tagsFiltered = this.props.tags.filter(\n                        (tag) =>\n                            (!this.props.selectedTags ||\n                                this.props.selectedTags.indexOf(tag[0]) < 0) &&\n                            (!request ||\n                                !request.length ||\n                                tag[1].toLocaleLowerCase().indexOf(request.toLocaleLowerCase()) >=\n                                    0)\n                    );\n                    if (!tagsFiltered || !tagsFiltered.length) {\n                        // no result, ask the user if he want to create a new tag\n                        if (!request || !request.length) {\n                            return [\n                                {\n                                    value: null,\n                                    label: _t(\"Start typing...\"),\n                                    classList: \"fst-italic\",\n                                },\n                            ];\n                        } else if (!this.props.canChangeTags) {\n                            return [\n                                {\n                                    value: null,\n                                    label: _t(\"No result\"),\n                                    classList: \"fst-italic\",\n                                },\n                            ];\n                        }\n\n                        return [\n                            {\n                                value: { toCreate: true, value: request },\n                                label: _t('Create \"%s\"', request),\n                                classList: \"o_field_property_dropdown_add\",\n                            },\n                        ];\n                    }\n                    return tagsFiltered.map((tag) => {\n                        return {\n                            value: tag[0],\n                            label: tag[1],\n                        };\n                    });\n                },\n            },\n        ];\n    }\n\n    /* --------------------------------------------------------\n     * Event handlers\n     * -------------------------------------------------------- */\n\n    /**\n     * Add one value in the current tag list values.\n     *\n     * @param {string | object} tagValue\n     *      Either\n     *      - {toCreate: true, value: label}, to create a new value\n     *      - value, to select an existing value\n     */\n    onOptionSelected(tagValue) {\n        if (!tagValue) {\n            // clicked on \"Start typing...\"\n            return;\n        }\n\n        if (tagValue.toCreate) {\n            this.onTagCreate(tagValue.value);\n        } else {\n            const selectedTags = this.selectedTags;\n            const newValue = [...selectedTags, tagValue];\n            this.props.onValueChange(newValue);\n        }\n    }\n\n    /**\n     * Ask to create a new tag that will be added in\n     * the definition and automatically selected.\n     *\n     * @param {string} newLabel\n     */\n    async onTagCreate(newLabel) {\n        if (!newLabel || !newLabel.length) {\n            return;\n        }\n\n        if (!(await this.props.checkDefinitionWriteAccess())) {\n            this.notification.add(\n                _t(\"You need to be able to edit parent first to add property tags\"),\n                { type: \"warning\" }\n            );\n            return;\n        }\n\n        const newValue = newLabel ? newLabel.toLowerCase().replace(\" \", \"_\") : \"\";\n\n        const existingTag = this.props.tags.find((tag) => tag[0] === newValue);\n        if (existingTag) {\n            this.notification.add(_t(\"This tag is already available\"), {\n                type: \"warning\",\n            });\n            return;\n        }\n\n        // cycle trough colors\n        let tagColor =\n            this.props.tags && this.props.tags.length\n                ? (this.props.tags[this.props.tags.length - 1][2] + 1) % ColorList.COLORS.length\n                : parseInt(Math.random() * ColorList.COLORS.length);\n        tagColor = tagColor || 1; // never select white by default\n\n        const newTag = [newValue, newLabel, tagColor];\n        const updatedTags = [...this.availableTags, newTag];\n        // automatically select the newly created tag\n        const newValues = [...this.props.selectedTags, newTag[0]];\n        this.props.onTagsChange(updatedTags, newValues);\n    }\n\n    /**\n     * Click on the delete button on the tag pill.\n     * The behavior is defined by the prop \"deleteAction\".\n     *\n     * If we use the component for the tag configuration, clicking on \"delete\"\n     * will remove the tags from the available tags. If we use the component\n     * the tag selection, it will unselect the tag.\n     *\n     * @param {string} deleteTag, ID of the tag to delete\n     */\n    onTagDelete(deleteTag) {\n        if (this.props.deleteAction === \"value\") {\n            // remove the tag from the value (but keep it in the options list)\n            const selectedTags = this.selectedTags;\n            const newValue = selectedTags.filter((tag) => tag !== deleteTag);\n            this.props.onValueChange(newValue);\n        } else {\n            // remove the tag from the options\n            const availableTags = this.availableTags;\n            this.props.onTagsChange(availableTags.filter((tag) => tag[0] !== deleteTag));\n        }\n    }\n\n    /**\n     * Click on a tag pill, open the color popover if we can change the tag definition.\n     *\n     * @param {event} event\n     * @param {string} tagId\n     * @param {integer} tagColor\n     */\n    onTagClick(event, tagId, tagColor) {\n        if (!this.props.canChangeTags) {\n            event.currentTarget.blur();\n            return;\n        }\n        this.popover.open(event.currentTarget, {\n            colors: [...Array(ColorList.COLORS.length).keys()],\n            tag: { id: tagId, colorIndex: tagColor },\n            switchTagColor: this.onTagColorSwitch.bind(this),\n        });\n    }\n\n    /**\n     * Ask to change the color of a tag.\n     *\n     * @param {integer} colorIndex\n     * @param {object} currentTag\n     */\n    onTagColorSwitch(colorIndex, currentTag) {\n        const availableTags = this.availableTags;\n        availableTags.find((tag) => tag[0] === currentTag.id)[2] = colorIndex;\n        this.props.onTagsChange(availableTags);\n\n        // close the color popover\n        this.popover.close();\n    }\n}\n\nexport class PropertyTagsField extends Component {\n    static template = \"web.PropertyTagsField\";\n    static components = { PropertyTags };\n    static props = { ...standardFieldProps };\n\n    get propertyTagsProps() {\n        return {\n            selectedTags: this.props.record.data[this.props.name] || [],\n            tags: this.props.record.fields[this.props.name].tags || [],\n            deleteAction: \"value\",\n            readonly: this.props.readonly,\n            canChangeTags: false,\n            onValueChange: (value) => {\n                this.props.record.update({ [this.props.name]: value });\n            },\n        };\n    }\n}\n\nexport const propertyTagsField = {\n    component: PropertyTagsField,\n};\n\nregistry.category(\"fields\").add(\"property_tags\", propertyTagsField);\n", "import { Component } from \"@odoo/owl\";\nimport { AutoComplete } from \"@web/core/autocomplete/autocomplete\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { DateTimeInput } from \"@web/core/datetime/datetime_input\";\nimport { Domain } from \"@web/core/domain\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport {\n    deserializeDate,\n    deserializeDateTime,\n    formatDate,\n    formatDateTime,\n    serializeDate,\n    serializeDateTime,\n} from \"@web/core/l10n/dates\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { TagsList } from \"@web/core/tags_list/tags_list\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { formatInteger, formatMany2one } from \"@web/views/fields/formatters\";\nimport { formatFloat } from \"@web/core/utils/numbers\";\nimport { m2oTupleFromData } from \"@web/views/fields/many2one/many2one_field\";\nimport { parseFloat, parseInteger } from \"@web/views/fields/parsers\";\nimport { Many2XAutocomplete, useOpenMany2XRecord } from \"@web/views/fields/relational_utils\";\nimport { PropertyTags } from \"./property_tags\";\nimport { imageUrl } from \"@web/core/utils/urls\";\n\n/**\n * Represent one property value.\n * Supports many types and instantiates the appropriate component for it.\n * - Text\n * - Integer\n * - Boolean\n * - Selection\n * - Datetime & Date\n * - Many2one\n * - Many2many\n * - Tags\n * - ...\n */\nexport class PropertyValue extends Component {\n    static template = \"web.PropertyValue\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n        CheckBox,\n        DateTimeInput,\n        Many2XAutocomplete,\n        TagsList,\n        AutoComplete,\n        PropertyTags,\n    };\n\n    static props = {\n        id: { type: String, optional: true },\n        type: { type: String, optional: true },\n        comodel: { type: String, optional: true },\n        domain: { type: String, optional: true },\n        string: { type: String, optional: true },\n        value: { optional: true },\n        context: { type: Object },\n        readonly: { type: Boolean, optional: true },\n        canChangeDefinition: { type: Boolean, optional: true },\n        checkDefinitionWriteAccess: { type: Function, optional: true },\n        selection: { type: Array, optional: true },\n        tags: { type: Array, optional: true },\n        onChange: { type: Function, optional: true },\n        onTagsChange: { type: Function, optional: true },\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n\n        this.openMany2X = useOpenMany2XRecord({\n            resModel: this.props.model,\n            activeActions: {\n                create: false,\n                createEdit: false,\n                write: true,\n            },\n            isToMany: false,\n            onRecordSaved: async (record) => {\n                if (!record) {\n                    return;\n                }\n                // maybe the record display name has changed\n                await record.load();\n                const recordData = m2oTupleFromData(record.data);\n                await this.onValueChange([{ id: recordData[0], name: recordData[1] }]);\n            },\n            fieldString: this.props.string,\n        });\n    }\n\n    /* --------------------------------------------------------\n     * Public methods / Getters\n     * -------------------------------------------------------- */\n\n    /**\n     * Return the value of the current property,\n     * that will be used by the sub-components.\n     *\n     * @returns {object}\n     */\n    get propertyValue() {\n        const value = this.props.value;\n\n        if (this.props.type === \"float\") {\n            // force to show at least 1 digit, even for integers\n            return value;\n        } else if (this.props.type === \"datetime\") {\n            const datetimeValue = typeof value === \"string\" ? deserializeDateTime(value) : value;\n            return datetimeValue && !datetimeValue.invalid ? datetimeValue : false;\n        } else if (this.props.type === \"date\") {\n            const dateValue = typeof value === \"string\" ? deserializeDate(value) : value;\n            return dateValue && !dateValue.invalid ? dateValue : false;\n        } else if (this.props.type === \"boolean\") {\n            return !!value;\n        } else if (this.props.type === \"selection\") {\n            const options = this.props.selection || [];\n            const option = options.find((option) => option[0] === value);\n            return option && option.length === 2 && option[0] ? option[0] : \"\";\n        } else if (this.props.type === \"many2one\") {\n            return !value || value.length !== 2 || !value[0] ? false : value;\n        } else if (this.props.type === \"many2many\") {\n            if (!value || !value.length) {\n                return [];\n            }\n\n            // Convert to TagsList component format\n            return value.map((many2manyValue) => {\n                const hasAccess = many2manyValue[1] !== null;\n                return {\n                    id: many2manyValue[0],\n                    comodel: this.props.comodel,\n                    text: hasAccess ? many2manyValue[1] : _t(\"No Access\"),\n                    onClick:\n                        hasAccess &&\n                        this.clickableRelational &&\n                        (async () => await this._openRecord(this.props.comodel, many2manyValue[0])),\n                    onDelete:\n                        !this.props.readonly &&\n                        hasAccess &&\n                        (() => this.onMany2manyDelete(many2manyValue[0])),\n                    colorIndex: 0,\n                    img:\n                        this.showAvatar && hasAccess\n                            ? imageUrl(this.props.comodel, many2manyValue[0], \"avatar_128\")\n                            : null,\n                };\n            });\n        } else if (this.props.type === \"tags\") {\n            return value || [];\n        }\n\n        return value;\n    }\n\n    /**\n     * Return the model domain (related to many2one and many2many properties).\n     *\n     * @returns {array}\n     */\n    get propertyDomain() {\n        if (!this.props.domain || !this.props.domain.length) {\n            return [];\n        }\n        let domain = new Domain(this.props.domain);\n        if (this.props.type === \"many2many\" && this.props.value) {\n            domain = Domain.and([\n                domain,\n                [[\"id\", \"not in\", this.props.value.map((rec) => rec[0])]],\n            ]);\n        }\n        return domain.toList();\n    }\n\n    /**\n     * Formatted value displayed in readonly mode.\n     *\n     * @returns {string}\n     */\n    get displayValue() {\n        const value = this.propertyValue;\n\n        if (this.props.type === \"many2one\" && value && value.length === 2) {\n            return formatMany2one(value);\n        } else if (this.props.type === \"integer\") {\n            return formatInteger(value || 0);\n        } else if (this.props.type === \"float\") {\n            return formatFloat(value || 0);\n        } else if (!value) {\n            return false;\n        } else if (this.props.type === \"datetime\" && value) {\n            return formatDateTime(value);\n        } else if (this.props.type === \"date\" && value) {\n            return formatDate(value);\n        } else if (this.props.type === \"selection\") {\n            return this.props.selection.find((option) => option[0] === value)[1];\n        }\n        return value.toString();\n    }\n\n    /**\n     * Return true if the relational properties are clickable.\n     *\n     * @returns {boolean}\n     */\n    get clickableRelational() {\n        return !this.env.config || this.env.config.viewType !== \"kanban\";\n    }\n\n    /**\n     * Return True if we need to display a avatar for the current property.\n     *\n     * @returns {boolean}\n     */\n    get showAvatar() {\n        return (\n            [\"many2one\", \"many2many\"].includes(this.props.type) &&\n            [\"res.users\", \"res.partner\"].includes(this.props.comodel)\n        );\n    }\n\n    /* --------------------------------------------------------\n     * Event handlers\n     * -------------------------------------------------------- */\n\n    /**\n     * Parse the value received by the sub-components and trigger an onChange event.\n     *\n     * @param {object} newValue\n     */\n    async onValueChange(newValue) {\n        if (this.props.type === \"datetime\") {\n            newValue = newValue && serializeDateTime(newValue);\n        } else if (this.props.type === \"date\") {\n            newValue = newValue && serializeDate(newValue);\n        } else if (this.props.type === \"integer\") {\n            try {\n                newValue = parseInteger(newValue) || 0;\n            } catch {\n                newValue = 0;\n            }\n        } else if (this.props.type === \"float\") {\n            try {\n                newValue = parseFloat(newValue) || 0;\n            } catch {\n                newValue = 0;\n            }\n        } else if ([\"many2one\", \"many2many\"].includes(this.props.type)) {\n            // {id: 5, name: 'Demo'} => [5, 'Demo']\n            newValue =\n                newValue && newValue.length && newValue[0].id\n                    ? [newValue[0].id, newValue[0].name]\n                    : false;\n\n            if (newValue && newValue[0] && newValue[1] === undefined) {\n                // The \"Search More\" option in the Many2XAutocomplete component\n                // only return the record ID, and not the name. But we need to name\n                // in the component props to be able to display it.\n                // Make a RPC call to resolve the display name of the record.\n                newValue = await this._nameGet(newValue[0]);\n            }\n\n            if (this.props.type === \"many2many\" && newValue) {\n                // add the record in the current many2many list\n                const currentValue = this.props.value || [];\n                const recordId = newValue[0];\n                const exists = currentValue.find((rec) => rec[0] === recordId);\n                if (exists) {\n                    return;\n                }\n                newValue = [...currentValue, newValue];\n            }\n        }\n\n        // trigger the onchange event to notify the parent component\n        this.props.onChange(newValue);\n    }\n\n    /**\n     * Open the form view of the current record.\n     *\n     * @param {event} event\n     */\n    async onMany2oneClick(event) {\n        if (this.props.readonly) {\n            event.stopPropagation();\n            await this._openRecord(this.props.comodel, this.propertyValue[0]);\n        }\n    }\n\n    /**\n     * Open the current many2one record form view in a modal.\n     */\n    onExternalLinkClick() {\n        return this.openMany2X({\n            resId: this.propertyValue[0],\n            forceModel: this.props.comodel,\n            context: this.context,\n        });\n    }\n\n    /**\n     * Removed a record from the many2many list.\n     *\n     * @param {integer} many2manyId\n     */\n    onMany2manyDelete(many2manyId) {\n        // deep copy\n        const currentValue = JSON.parse(JSON.stringify(this.props.value || []));\n        const newValue = currentValue.filter((value) => value[0] !== many2manyId);\n        this.props.onChange(newValue);\n    }\n\n    /**\n     * Ask to create a record from a relational property.\n     *\n     * @param {string} name\n     * @param {object} params\n     */\n    async onQuickCreate(name, params = {}) {\n        const result = await this.orm.call(this.props.comodel, \"name_create\", [name], {\n            context: this.props.context,\n        });\n        this.onValueChange([{ id: result[0], name: result[1] }]);\n    }\n\n    /* --------------------------------------------------------\n     * Private methods\n     * -------------------------------------------------------- */\n\n    /**\n     * Open the form view of the given record id / model.\n     *\n     * @param {string} recordModel\n     * @param {integer} recordId\n     */\n    async _openRecord(recordModel, recordId) {\n        const action = await this.orm.call(recordModel, \"get_formview_action\", [[recordId]], {\n            context: this.props.context,\n        });\n\n        this.action.doAction(action);\n    }\n\n    /**\n     * Get the display name of the given record.\n     * Model is taken from the current selected model.\n     *\n     * @param {string} recordId\n     * @returns {array} [record id, record name]\n     */\n    async _nameGet(recordId) {\n        const result = await this.orm.read(this.props.comodel, [recordId], [\"display_name\"], {\n            context: this.props.context,\n        });\n        return [result[0].id, result[0].display_name];\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { getFieldDomain } from \"@web/model/relational_model/utils\";\nimport { useSpecialData } from \"@web/views/fields/relational_utils\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nlet nextId = 0;\nexport class RadioField extends Component {\n    static template = \"web.RadioField\";\n    static props = {\n        ...standardFieldProps,\n        orientation: { type: String, optional: true },\n        label: { type: String, optional: true },\n        domain: { type: [Array, Function], optional: true },\n    };\n    static defaultProps = {\n        orientation: \"vertical\",\n    };\n\n    setup() {\n        this.id = `radio_field_${nextId++}`;\n        this.type = this.props.record.fields[this.props.name].type;\n        if (this.type === \"many2one\") {\n            this.specialData = useSpecialData(async (orm, props) => {\n                const { relation } = props.record.fields[props.name];\n                const domain = getFieldDomain(props.record, props.name, props.domain);\n                const kwargs = {\n                    specification: { display_name: 1 },\n                    domain,\n                };\n                const { records } = await orm.call(relation, \"web_search_read\", [], kwargs);\n                return records.map((record) => [record.id, record.display_name]);\n            });\n        }\n    }\n\n    get items() {\n        switch (this.type) {\n            case \"selection\":\n                return this.props.record.fields[this.props.name].selection;\n            case \"many2one\": {\n                return this.specialData.data;\n            }\n            default:\n                return [];\n        }\n    }\n    get value() {\n        switch (this.type) {\n            case \"selection\":\n                return this.props.record.data[this.props.name];\n            case \"many2one\":\n                return Array.isArray(this.props.record.data[this.props.name])\n                    ? this.props.record.data[this.props.name][0]\n                    : this.props.record.data[this.props.name];\n            default:\n                return null;\n        }\n    }\n\n    /**\n     * @param {any} value\n     */\n    onChange(value) {\n        switch (this.type) {\n            case \"selection\":\n                this.props.record.update({ [this.props.name]: value[0] });\n                break;\n            case \"many2one\":\n                this.props.record.update({ [this.props.name]: value });\n                break;\n        }\n    }\n}\n\nexport const radioField = {\n    component: RadioField,\n    displayName: _t(\"Radio\"),\n    supportedOptions: [\n        {\n            label: _t(\"Display horizontally\"),\n            name: \"horizontal\",\n            type: \"boolean\",\n        },\n    ],\n    supportedTypes: [\"many2one\", \"selection\"],\n    isEmpty: (record, fieldName) => record.data[fieldName] === false,\n    extractProps: ({ options, string }, dynamicInfo) => ({\n        orientation: options.horizontal ? \"horizontal\" : \"vertical\",\n        label: string,\n        domain: dynamicInfo.domain,\n    }),\n};\n\nregistry.category(\"fields\").add(\"radio\", radioField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\nimport { many2OneField, Many2OneField } from \"../many2one/many2one_field\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\n/**\n * @typedef ReferenceValue\n * @property {string} resModel\n * @property {number} resId\n * @property {string} displayName\n */\n\n/**\n * 1. Reference field is a char field\n * 2. Reference widget has model_field prop\n * 3. Standard case\n */\n\n/**\n * This class represents a reference field widget. It can be used to display\n * a reference field OR a char field.\n * The res_model of the relation is defined either by the reference field itself\n * or by the model_field prop.\n *\n * 1) Reference field is a char field\n * We have to fetch the display name (name_get) of the referenced record.\n *\n * 2) Reference widget has model_field prop\n * We have to fetch the technical name of the co model.\n *\n * 3) Standard case\n * The value is already in record.data[fieldName]\n */\nexport class ReferenceField extends Component {\n    static template = \"web.ReferenceField\";\n    static components = {\n        Many2OneField,\n    };\n    static props = {\n        ...Many2OneField.props,\n        hideModel: { type: Boolean, optional: true },\n        modelField: { type: String, optional: true },\n    };\n    static defaultProps = {\n        ...Many2OneField.defaultProps,\n    };\n\n    setup() {\n        /** @type {{formattedCharValue?: ReferenceValue, modelName?: string}} */\n        this.state = useState({\n            formattedCharValue: undefined, // Value extracted from reference char field\n            modelName: undefined, // Name get of the value of the model field\n            currentRelation: undefined,\n        });\n        if (this._isCharField(this.props)) {\n            /** Fetch the display name of the record referenced by the field */\n            let currentValue = undefined;\n            useRecordObserver(async (record, props) => {\n                if (currentValue !== record.data[props.name]) {\n                    this.state.formattedCharValue = await this._fetchReferenceCharData(props);\n                    currentValue = record.data[props.name];\n                }\n            });\n        } else if (this.props.modelField) {\n            /** Fetch the technical name of the co model */\n            useRecordObserver(async (record, props) => {\n                if (this.currentModelId !== record.data[props.modelField]?.[0]) {\n                    this.state.modelName = await this._fetchModelTechnicalName(props);\n                    if (this.currentModelId !== undefined) {\n                        record.update({ [props.name]: false });\n                    }\n                    this.currentModelId = record.data[props.modelField]?.[0];\n                }\n            });\n        }\n    }\n\n    get m2oProps() {\n        const value = this.getValue();\n        const p = {\n            ...this.props,\n            relation: this.getRelation(),\n            value: value && [value.resId, value.displayName],\n            update: this.updateM2O.bind(this),\n        };\n        delete p.hideModel;\n        delete p.modelField;\n        return p;\n    }\n    get selection() {\n        if (!this._isCharField(this.props) && !this.hideModelSelector) {\n            return this.props.record.fields[this.props.name].selection;\n        }\n        return [];\n    }\n\n    get relation() {\n        return this.getRelation();\n    }\n\n    get hideModelSelector() {\n        return this.props.hideModel || this.props.modelField;\n    }\n\n    getRelation() {\n        const modelName = this.getModelName();\n        if (modelName) {\n            return modelName;\n        }\n\n        const value = this.getValue();\n        if (value && value.resModel) {\n            return value.resModel;\n        } else {\n            return this.state.currentRelation;\n        }\n    }\n\n    /**\n     * @returns {ReferenceValue|false}\n     */\n    getValue() {\n        if (this._isCharField(this.props)) {\n            return this.state.formattedCharValue;\n        } else {\n            return this.props.record.data[this.props.name];\n        }\n    }\n\n    /**\n     * @returns {string|undefined}\n     */\n    getModelName() {\n        return this.hideModelSelector && this.state.modelName;\n    }\n\n    updateModel(value) {\n        this.state.currentRelation = value;\n        this.props.record.update({ [this.props.name]: false });\n    }\n\n    updateM2O(data) {\n        const value = data[this.props.name];\n        const resModel = this.state.currentRelation || this.getRelation();\n        this.props.record.update({\n            [this.props.name]: value && {\n                resModel,\n                resId: value[0],\n                displayName: value[1],\n            },\n        });\n    }\n\n    /**\n     * Return true if the reference field is a char field.\n     */\n    _isCharField(props) {\n        return props.record.fields[props.name].type === \"char\";\n    }\n\n    /**\n     * Fetch special data if the reference field is a char field.\n     * It fetches the display name of the record.\n     *\n     * @returns {Promise<{ resId: number, resModel: string, displayName: string }|false>}\n     */\n    async _fetchReferenceCharData(props) {\n        const recordData = props.record.data[props.name];\n        if (!recordData) {\n            return false;\n        }\n        const [resModel, _resId] = recordData.split(\",\");\n        const resId = parseInt(_resId, 10);\n        if (resModel && resId) {\n            const { specialDataCaches, orm } = props.record.model;\n            const key = `__reference__name_get-${recordData}`;\n            if (!specialDataCaches[key]) {\n                specialDataCaches[key] = orm.read(resModel, [resId], [\"display_name\"]);\n            }\n            const result = await specialDataCaches[key];\n            return {\n                resId,\n                resModel,\n                displayName: result[0].display_name,\n            };\n        }\n        return false;\n    }\n\n    /**\n     * Ensure that the modelField is a many2one to ir.model\n     */\n    _assertMany2OneToIrModel(props) {\n        const field = props.modelField && props.record.fields[props.modelField];\n        if (field && (field.type !== \"many2one\" || field.relation !== \"ir.model\")) {\n            throw new Error(\n                `The model_field (${props.modelField}) of the reference field ${props.name} must be a many2one('ir.model').`\n            );\n        }\n    }\n\n    /**\n     * Fetch the technical name of the model which is selected in the modelField\n     * props\n     *\n     * @returns {Promise<string|false>}\n     */\n    async _fetchModelTechnicalName(props) {\n        this._assertMany2OneToIrModel(props);\n        const record = props.record;\n        const modelId = record.data[props.modelField]?.[0];\n        if (!modelId) {\n            return false;\n        }\n        const { specialDataCaches, orm } = props.record.model;\n        const key = `__reference__ir_model-${modelId}`;\n        if (!specialDataCaches[key]) {\n            specialDataCaches[key] = orm.read(\"ir.model\", [modelId], [\"model\"]);\n        }\n        const result = await specialDataCaches[key];\n        return result[0].model;\n    }\n}\n\nexport const referenceField = {\n    component: ReferenceField,\n    displayName: _t(\"Reference\"),\n    supportedOptions: [\n        {\n            label: _t(\"Hide model\"),\n            name: \"hide_model\",\n            type: \"boolean\",\n        },\n        {\n            label: _t(\"Model field\"),\n            name: \"model_field\",\n            type: \"field\",\n            availableTypes: [\"many2one\"],\n        },\n    ],\n    supportedTypes: [\"reference\", \"char\"],\n    extractProps({ options }) {\n        /*\n        1 - <field name=\"ref\" options=\"{'model_field': 'model_id'}\" />\n        2 - <field name=\"ref\" options=\"{'hide_model': True}\" />\n        3 - <field name=\"ref\" options=\"{'model_field': 'model_id' 'hide_model': True}\" />\n        4 - <field name=\"ref\"/>\n\n        We want to display the model selector only in the 4th case.\n        */\n        const props = many2OneField.extractProps(...arguments);\n        props.hideModel = !!options.hide_model;\n        props.modelField = options.model_field;\n        return props;\n    },\n};\n\nregistry.category(\"fields\").add(\"reference\", referenceField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { AutoComplete } from \"@web/core/autocomplete/autocomplete\";\nimport { makeContext } from \"@web/core/context\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { Domain } from \"@web/core/domain\";\nimport { RPCError } from \"@web/core/network/rpc\";\nimport { Cache } from \"@web/core/utils/cache\";\nimport {\n    useBus,\n    useChildRef,\n    useForwardRefToParent,\n    useOwnedDialogs,\n    useService,\n} from \"@web/core/utils/hooks\";\nimport { createElement, parseXML } from \"@web/core/utils/xml\";\nimport { FormArchParser } from \"@web/views/form/form_arch_parser\";\nimport { loadSubViews, useFormViewInDialog } from \"@web/views/form/form_controller\";\nimport { FormRenderer } from \"@web/views/form/form_renderer\";\nimport { extractFieldsFromArchInfo, useRecordObserver } from \"@web/model/relational_model/utils\";\nimport { computeViewClassName, isNull } from \"@web/views/utils\";\nimport { ViewButton } from \"@web/views/view_button/view_button\";\nimport { executeButtonCallback, useViewButtons } from \"@web/views/view_button/view_button_hook\";\nimport { FormViewDialog } from \"@web/views/view_dialogs/form_view_dialog\";\nimport { SelectCreateDialog } from \"@web/views/view_dialogs/select_create_dialog\";\n\n/**\n * @typedef {Object} RelationalActiveActions {\n * @property {\"x2m\"} type\n * @property {boolean} create\n * @property {boolean} createEdit\n * @property {boolean} delete\n * @property {boolean} [link]\n * @property {boolean} [unlink]\n * @property {boolean} [write]\n * @property {Function | null} onDelete\n *\n * @typedef {import(\"services\").Services} Services\n */\n\nimport {\n    Component,\n    onWillUpdateProps,\n    useComponent,\n    useEffect,\n    useEnv,\n    useState,\n    useSubEnv,\n} from \"@odoo/owl\";\n\n//\n// Commons\n//\nexport function useSelectCreate({ resModel, activeActions, onSelected, onCreateEdit, onUnselect }) {\n    const addDialog = useOwnedDialogs();\n\n    function selectCreate({ domain, context, filters, title }) {\n        addDialog(SelectCreateDialog, {\n            title: title || _t(\"Select records\"),\n            noCreate: !activeActions.create,\n            multiSelect: \"link\" in activeActions ? activeActions.link : false, // LPE Fixme\n            resModel,\n            context,\n            domain,\n            onSelected,\n            onCreateEdit: () => onCreateEdit({ context }),\n            dynamicFilters: filters,\n            onUnselect,\n        });\n    }\n    return selectCreate;\n}\n\nconst STANDARD_ACTIVE_ACTIONS = [\"create\", \"createEdit\", \"delete\", \"link\", \"unlink\", \"write\"];\n\n/**\n * FIXME: this should somehow be merged with 'getActiveActions' (@web/views/utils.js)\n * Also I don't think storing a function in a collection of booleans is a good idea...\n *\n * @param {Object} params\n * @param {string} params.fieldType\n * @param {Record<string, boolean>} [params.subViewActiveActions={}]\n * @param {Object} [params.crudOptions={}]\n * @param {(props: Record<string, any>) => Record<any, any>} [params.getEvalParams=() => ({})]\n * @returns {RelationalActiveActions}\n */\nexport function useActiveActions({\n    fieldType,\n    subViewActiveActions = {},\n    crudOptions = {},\n    getEvalParams = () => ({}),\n}) {\n    const compute = ({ evalContext = {}, readonly = true }) => {\n        /** @type {RelationalActiveActions} */\n        const result = { type: fieldType, onDelete: null };\n        const evalAction = (actionName) => evals[actionName](evalContext);\n\n        // We need to take care of tags \"control\" and \"create\" to set create stuff\n        result.create = !readonly && evalAction(\"create\");\n        result.createEdit = !readonly && result.create && crudOptions.createEdit; // always a boolean\n        result.edit = crudOptions.edit;\n        result.delete = !readonly && evalAction(\"delete\");\n\n        if (isMany2Many) {\n            result.link = !readonly && evalAction(\"link\");\n            result.unlink = !readonly && evalAction(\"unlink\");\n            result.write = evalAction(\"write\");\n        }\n\n        if (result.unlink || (!isMany2Many && result.delete)) {\n            result.onDelete = crudOptions.onDelete;\n        }\n\n        return result;\n    };\n\n    const props = useComponent().props;\n    const isMany2Many = fieldType === \"many2many\";\n\n    // Define eval functions\n    const evals = {};\n    for (const actionName of STANDARD_ACTIVE_ACTIONS) {\n        let evalFn = () => actionName !== \"write\";\n        if (!isNull(crudOptions[actionName])) {\n            const action = crudOptions[actionName];\n            evalFn = (evalContext) => Boolean(action && new Domain(action).contains(evalContext));\n        }\n\n        if (actionName in subViewActiveActions) {\n            const viewActiveAction = subViewActiveActions[actionName];\n            evals[actionName] = (evalContext) => viewActiveAction && evalFn(evalContext);\n        } else {\n            evals[actionName] = evalFn;\n        }\n    }\n\n    // Compute active actions\n    const activeActions = compute(getEvalParams(props));\n    onWillUpdateProps((nextProps) => {\n        Object.assign(activeActions, compute(getEvalParams(nextProps)));\n    });\n\n    return activeActions;\n}\n\n/**\n * @template T, [Props=any], [Env=any]\n * @param {(orm: Services[\"orm\"], props: Component<Props, Env>[\"props\"]) => Promise<T>} loadFn\n */\nexport function useSpecialData(loadFn) {\n    const component = useComponent();\n    const record = component.props.record;\n    const key = `${record.resModel}-${component.props.name}`;\n    const { specialDataCaches } = record.model;\n    const orm = component.env.services.orm;\n    const ormWithCache = Object.create(orm);\n    if (!specialDataCaches[key]) {\n        specialDataCaches[key] = new Cache(\n            (...args) => orm.call(...args),\n            (...path) => JSON.stringify(path)\n        );\n    }\n    ormWithCache.call = (...args) => specialDataCaches[key].read(...args);\n\n    /** @type {{ data: Record<string, T> }} */\n    const result = useState({ data: {} });\n    useRecordObserver(async (record, props) => {\n        result.data = await loadFn(ormWithCache, { ...props, record });\n    });\n    onWillUpdateProps(async (props) => {\n        // useRecordObserver callback is not called when the record doesn't change\n        if (props.record.id === component.props.record.id) {\n            result.data = await loadFn(ormWithCache, props);\n        }\n    });\n    return result;\n}\n\n//\n// Many2X\n//\n\nexport class Many2XAutocomplete extends Component {\n    static template = \"web.Many2XAutocomplete\";\n    static components = { AutoComplete };\n    static props = {\n        value: { type: String, optional: true },\n        activeActions: Object,\n        context: { type: Object, optional: true },\n        nameCreateField: { type: String, optional: true },\n        setInputFloats: { type: Function, optional: true },\n        update: Function,\n        resModel: String,\n        getDomain: Function,\n        searchLimit: { type: Number, optional: true },\n        quickCreate: { type: [Function, { value: null }], optional: true },\n        noSearchMore: { type: Boolean, optional: true },\n        searchMoreLimit: { type: Number, optional: true },\n        fieldString: String,\n        id: { type: String, optional: true },\n        placeholder: { type: String, optional: true },\n        autoSelect: { type: Boolean, optional: true },\n        isToMany: { type: Boolean, optional: true },\n        autocomplete_container: { type: Function, optional: true },\n        dropdown: { type: Boolean, optional: true },\n        autofocus: { type: Boolean, optional: true },\n        getOptionClassnames: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        searchLimit: 7,\n        searchMoreLimit: 320,\n        nameCreateField: \"name\",\n        value: \"\",\n        setInputFloats: () => {},\n        quickCreate: null,\n        context: {},\n        dropdown: true,\n        getOptionClassnames: () => \"\",\n    };\n    setup() {\n        this.orm = useService(\"orm\");\n\n        this.autoCompleteContainer = useForwardRefToParent(\"autocomplete_container\");\n        const { activeActions, resModel, update, isToMany, fieldString } = this.props;\n\n        this.openMany2X = useOpenMany2XRecord({\n            resModel,\n            activeActions,\n            isToMany,\n            onRecordSaved: (record) => {\n                return update([{ ...record.data, id: record.resId }]);\n            },\n            onRecordDiscarded: () => {\n                if (!isToMany) {\n                    this.props.update(false);\n                }\n            },\n            fieldString,\n            onClose: () => {\n                const autoCompleteInput = this.autoCompleteContainer.el.querySelector(\"input\");\n\n                // There are two cases:\n                // 1. Value is the same as the input: it means the autocomplete has re-rendered with the right value\n                //    This is in case we saved the record, triggering all the interface to update.\n                // 2. Value is different from the input: it means the input has a manually entered value and nothing\n                //    happened, that is, we discarded the changes\n                if (this.props.value !== autoCompleteInput.value) {\n                    autoCompleteInput.value = \"\";\n                }\n                autoCompleteInput.focus();\n            },\n        });\n\n        this.selectCreate = useSelectCreate({\n            resModel,\n            activeActions,\n            onSelected: (resId) => {\n                const resIds = Array.isArray(resId) ? resId : [resId];\n                const values = resIds.map((id) => ({ id }));\n                return update(values);\n            },\n            onCreateEdit: ({ context }) => this.openMany2X({ context }),\n            onUnselect: isToMany ? undefined : () => update(),\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    get activeActions() {\n        return this.props.activeActions || {};\n    }\n\n    getCreationContext(value) {\n        return makeContext([\n            this.props.context,\n            { [`default_${this.props.nameCreateField}`]: value },\n        ]);\n    }\n    onInput({ inputValue }) {\n        if (!this.props.value || this.props.value !== inputValue) {\n            this.props.setInputFloats(true);\n        }\n    }\n    onCancel() {\n        this.props.setInputFloats(false);\n    }\n\n    onSelect(option, params = {}) {\n        if (option.action) {\n            return option.action(params);\n        }\n        const record = {\n            id: option.value,\n            display_name: option.displayName,\n        };\n        this.props.update([record], params);\n    }\n\n    abortableSearch(name) {\n        const originalPromise = this.search(name);\n        return {\n            promise: originalPromise,\n            abort: originalPromise.abort ? originalPromise.abort.bind(originalPromise) : () => {},\n        };\n    }\n    search(name) {\n        return this.orm.call(this.props.resModel, \"name_search\", [], {\n            name: name,\n            operator: \"ilike\",\n            args: this.props.getDomain(),\n            limit: this.props.searchLimit + 1,\n            context: this.props.context,\n        });\n    }\n    mapRecordToOption(result) {\n        return {\n            value: result[0],\n            label: result[1] ? result[1].split(\"\\n\")[0] : _t(\"Unnamed\"),\n            displayName: result[1],\n            classList: this.props.getOptionClassnames({ id: result[0], display_name: result[1] }),\n        };\n    }\n    async loadOptionsSource(request) {\n        if (this.lastProm) {\n            this.lastProm.abort(false);\n        }\n        this.lastProm = this.abortableSearch(request);\n        const records = await this.lastProm.promise;\n\n        const options = records.map((result) => this.mapRecordToOption(result));\n\n        if (this.props.quickCreate && request.length) {\n            options.push({\n                label: _t('Create \"%s\"', request),\n                classList: \"o_m2o_dropdown_option o_m2o_dropdown_option_create\",\n                action: async (params) => {\n                    try {\n                        await this.props.quickCreate(request, params);\n                    } catch (e) {\n                        if (\n                            e instanceof RPCError &&\n                            e.exceptionName === \"odoo.exceptions.ValidationError\"\n                        ) {\n                            return this.openMany2X({\n                                context: this.getCreationContext(request),\n                                nextRecordsContext: this.props.context,\n                            });\n                        }\n                        throw e;\n                    }\n                },\n            });\n        }\n\n        if (!this.props.noSearchMore && records.length > 0) {\n            options.push({\n                label: this.SearchMoreButtonLabel,\n                action: this.onSearchMore.bind(this, request),\n                classList: \"o_m2o_dropdown_option o_m2o_dropdown_option_search_more\",\n            });\n        }\n\n        const canCreateEdit =\n            \"createEdit\" in this.activeActions\n                ? this.activeActions.createEdit\n                : this.activeActions.create;\n        if (!request.length && !this.props.value && (this.props.quickCreate || canCreateEdit)) {\n            options.push({\n                label: _t(\"Start typing...\"),\n                classList: \"o_m2o_start_typing\",\n                unselectable: true,\n            });\n        }\n\n        if (request.length && canCreateEdit) {\n            options.push({\n                label: _t(\"Create and edit...\"),\n                classList: \"o_m2o_dropdown_option o_m2o_dropdown_option_create_edit\",\n                action: () =>\n                    this.openMany2X({\n                        context: this.getCreationContext(request),\n                        nextRecordsContext: this.props.context,\n                    }),\n            });\n        }\n\n        if (!records.length && !this.activeActions.createEdit && !this.props.quickCreate) {\n            options.push({\n                label: _t(\"No records\"),\n                classList: \"o_m2o_no_result\",\n                unselectable: true,\n            });\n        }\n\n        return options;\n    }\n\n    get SearchMoreButtonLabel() {\n        return _t(\"Search More...\");\n    }\n\n    async onBarcodeSearch() {\n        const autoCompleteInput = this.autoCompleteContainer.el.querySelector(\"input\");\n        return this.onSearchMore(autoCompleteInput.value);\n    }\n\n    async onSearchMore(request) {\n        const { resModel, getDomain, context, fieldString } = this.props;\n\n        const domain = getDomain();\n        let dynamicFilters = [];\n        if (request.length) {\n            const nameGets = await this.orm.call(resModel, \"name_search\", [], {\n                name: request,\n                args: domain,\n                operator: \"ilike\",\n                limit: this.props.searchMoreLimit,\n                context,\n            });\n\n            dynamicFilters = [\n                {\n                    description: _t(\"Quick search: %s\", request),\n                    domain: [[\"id\", \"in\", nameGets.map((nameGet) => nameGet[0])]],\n                },\n            ];\n        }\n\n        const title = _t(\"Search: %s\", fieldString);\n        this.selectCreate({\n            domain,\n            context,\n            filters: dynamicFilters,\n            title,\n        });\n    }\n\n    onChange({ inputValue }) {\n        if (!inputValue.length) {\n            this.props.update(false);\n        }\n    }\n}\n\nexport class AvatarMany2XAutocomplete extends Many2XAutocomplete {\n    mapRecordToOption(result) {\n        return {\n            ...super.mapRecordToOption(result),\n            resModel: this.props.resModel,\n        };\n    }\n    get optionsSource() {\n        return {\n            ...super.optionsSource,\n            optionTemplate: \"web.AvatarMany2XAutocomplete\",\n        };\n    }\n}\n\nexport function useOpenMany2XRecord({\n    resModel,\n    onRecordSaved,\n    onRecordDiscarded,\n    fieldString,\n    activeActions,\n    isToMany,\n    onClose = (isNew) => {},\n}) {\n    const addDialog = useOwnedDialogs();\n    const orm = useService(\"orm\");\n\n    return async function openDialog(\n        { resId = false, forceModel = null, title, context, nextRecordsContext },\n        immediate = false\n    ) {\n        const model = forceModel || resModel;\n        let viewId;\n        if (resId !== false) {\n            viewId = await orm.call(model, \"get_formview_id\", [[resId]], {\n                context,\n            });\n        }\n\n        let resolve = () => {};\n        if (!title) {\n            title = resId ? _t(\"Open: %s\", fieldString) : _t(\"Create %s\", fieldString);\n        }\n\n        const { create: canCreate, write: canWrite } = activeActions;\n        const mode = (resId ? canWrite : canCreate) ? \"edit\" : \"readonly\";\n\n        addDialog(\n            FormViewDialog,\n            {\n                preventCreate: !canCreate,\n                preventEdit: !canWrite,\n                title,\n                context,\n                nextRecordsContext,\n                mode,\n                resId,\n                resModel: model,\n                viewId,\n                onRecordSaved,\n                onRecordDiscarded,\n                isToMany,\n            },\n            {\n                onClose: () => {\n                    resolve();\n                    const isNew = !resId;\n                    onClose(isNew);\n                },\n            }\n        );\n\n        if (!immediate) {\n            return new Promise((_resolve) => {\n                resolve = _resolve;\n            });\n        }\n    };\n}\n\n//\n// X2Many\n//\n\nexport class X2ManyFieldDialog extends Component {\n    static template = \"web.X2ManyFieldDialog\";\n    static components = { Dialog, FormRenderer, ViewButton };\n    static props = {\n        archInfo: Object,\n        close: Function,\n        record: Object,\n        addNew: Function,\n        save: Function,\n        title: String,\n        delete: { optional: true },\n        deleteButtonLabel: { optional: true },\n        config: Object,\n    };\n    setup() {\n        this.actionService = useService(\"action\");\n        this.archInfo = this.props.archInfo;\n        this.record = this.props.record;\n        this.title = this.props.title;\n        this.contentClass = computeViewClassName(\"form\", this.archInfo.xmlDoc);\n        useSubEnv({ config: this.props.config });\n        this.env.dialogData.dismiss = () => this.discard();\n\n        useBus(this.record.model.bus, \"update\", () => this.render(true));\n\n        this.modalRef = useChildRef();\n\n        const reload = () => this.record.load();\n\n        useViewButtons(this.modalRef, {\n            reload,\n            beforeExecuteAction: this.beforeExecuteActionButton.bind(this),\n        }); // maybe pass the model directly in props\n\n        this.canCreate = !this.record.resId;\n\n        if (this.archInfo.xmlDoc.querySelector(\"footer:not(field footer)\")) {\n            this.archInfo = { ...this.archInfo, xmlDoc: this.archInfo.xmlDoc.cloneNode(true) };\n            this.footerArchInfo = Object.assign({}, this.archInfo);\n            this.footerArchInfo.xmlDoc = createElement(\"t\");\n            this.footerArchInfo.xmlDoc.append(\n                ...this.archInfo.xmlDoc.querySelectorAll(\"footer:not(field footer)\")\n            );\n            this.footerArchInfo.arch = this.footerArchInfo.xmlDoc.outerHTML;\n            this.archInfo.arch = this.archInfo.xmlDoc.outerHTML;\n        }\n\n        const { autofocusFieldId, disableAutofocus } = this.archInfo;\n        if (!disableAutofocus) {\n            // to simplify\n            useEffect(\n                (isInEdition) => {\n                    let elementToFocus;\n                    if (isInEdition) {\n                        elementToFocus =\n                            (autofocusFieldId &&\n                                this.modalRef.el.querySelector(`#${autofocusFieldId}`)) ||\n                            this.modalRef.el.querySelector(\".o_field_widget input\");\n                    } else {\n                        elementToFocus = this.modalRef.el.querySelector(\"button.btn-primary\");\n                    }\n                    if (elementToFocus) {\n                        elementToFocus.focus();\n                    } else {\n                        this.modalRef.el.focus();\n                    }\n                },\n                () => [this.record.isInEdition]\n            );\n        }\n        useFormViewInDialog();\n    }\n\n    get dialogProps() {\n        const props = {\n            title: this.title,\n            withBodyPadding: false,\n            modalRef: this.modalRef,\n            contentClass: this.contentClass,\n        };\n        if (!this.record.isNew) {\n            props.onExpand = async () => {\n                await this.save({ saveAndNew: false });\n                this.actionService.doAction({\n                    type: \"ir.actions.act_window\",\n                    res_model: this.props.record.resModel,\n                    res_id: this.props.record.resId,\n                    views: [[false, \"form\"]],\n                });\n            };\n        }\n        return props;\n    }\n\n    async beforeExecuteActionButton(clickParams) {\n        if (clickParams.special !== \"cancel\") {\n            return this.record.save();\n        }\n    }\n\n    async discard() {\n        if (this.record.isInEdition) {\n            await this.record.discard();\n        }\n        this.props.close();\n    }\n\n    save({ saveAndNew }) {\n        return executeButtonCallback(this.modalRef.el, async () => {\n            if (await this.record.checkValidity({ displayNotification: true })) {\n                await this.props.save(this.record);\n                if (saveAndNew) {\n                    await this.record.switchMode(\"readonly\");\n                    this.record = await this.props.addNew();\n                }\n            } else {\n                return false;\n            }\n            if (!saveAndNew) {\n                this.props.close();\n            }\n            return true;\n        });\n    }\n\n    async remove() {\n        await this.props.delete();\n        this.props.close();\n    }\n\n    async saveAndNew() {\n        const saved = await this.save({ saveAndNew: true });\n        if (saved) {\n            if (this.title) {\n                this.title = this.title.replace(_t(\"Open:\"), _t(\"New:\"));\n            }\n            this.render(true);\n        }\n    }\n}\n\nasync function getFormViewInfo({ list, context, activeField, viewService, env }) {\n    let formArchInfo = activeField.views.form;\n    let fields = activeField.fields;\n    const comodel = list.resModel;\n    if (!formArchInfo) {\n        const {\n            fields: formFields,\n            relatedModels,\n            views,\n        } = await viewService.loadViews({\n            context: makeContext([list.context, context]),\n            resModel: comodel,\n            views: [[false, \"form\"]],\n        });\n        const xmlDoc = parseXML(views.form.arch);\n        formArchInfo = new FormArchParser().parse(xmlDoc, relatedModels, comodel);\n        // Fields that need to be defined are the ones in the form view, this is natural,\n        // plus the ones that the list record has, that is, present in either the list arch\n        // or the kanban arch of the one2many field\n        fields = { ...list.fields, ...formFields }; // FIXME: update in place?\n    }\n\n    await loadSubViews(\n        formArchInfo.fieldNodes,\n        fields,\n        {}, // context\n        comodel,\n        viewService,\n        env.isSmall\n    );\n\n    return { archInfo: formArchInfo, fields };\n}\n\nexport function useAddInlineRecord({ addNew }) {\n    let creatingRecord = false;\n\n    async function addInlineRecord({ context, editable }) {\n        if (!creatingRecord) {\n            creatingRecord = true;\n            try {\n                await addNew({ context, mode: \"edit\", position: editable });\n            } finally {\n                creatingRecord = false;\n            }\n        }\n    }\n    return addInlineRecord;\n}\n\nexport function useOpenX2ManyRecord({\n    activeField, // TODO: this should be renamed (object with keys \"viewMode\", \"views\" and \"string\")\n    activeActions,\n    getList,\n    updateRecord,\n    saveRecord,\n    isMany2Many,\n}) {\n    const viewService = useService(\"view\");\n    const env = useEnv();\n    const component = useComponent();\n\n    const addDialog = useOwnedDialogs();\n    const viewMode = activeField.viewMode;\n\n    async function openRecord({ record, mode, context, title, onClose }) {\n        if (!title) {\n            title = record\n                ? _t(\"Open: %s\", activeField.string)\n                : _t(\"Create %s\", activeField.string);\n        }\n        const list = getList();\n        const { archInfo, fields: _fields } = await getFormViewInfo({\n            list,\n            context,\n            activeField,\n            viewService,\n            env,\n        });\n        if (!component.props.record.isInEdition) {\n            archInfo.activeActions.edit = false;\n        }\n\n        const { activeFields, fields } = extractFieldsFromArchInfo(archInfo, _fields);\n\n        let deleteRecord;\n        let deleteButtonLabel = undefined;\n        const isDuplicate = !!record;\n\n        const params = { activeFields, fields, mode };\n        if (record) {\n            const { delete: canDelete, onDelete } = activeActions;\n            deleteRecord = viewMode === \"kanban\" && canDelete ? () => onDelete(record) : null;\n            deleteButtonLabel = activeActions.type === \"one2many\" ? _t(\"Delete\") : _t(\"Remove\");\n        } else {\n            params.context = makeContext([list.context, context]);\n            params.withoutParent = isMany2Many;\n        }\n        record = await list.extendRecord(params, record);\n\n        const _onClose = () => {\n            list.editedRecord?.switchMode(\"readonly\");\n            onClose?.();\n        };\n\n        addDialog(\n            X2ManyFieldDialog,\n            {\n                config: env.config,\n                archInfo,\n                record,\n                addNew: () => {\n                    return getList().extendRecord(params);\n                },\n                save: (rec) => {\n                    if (isDuplicate && rec.id === record.id) {\n                        return updateRecord(rec);\n                    } else {\n                        return saveRecord(rec);\n                    }\n                },\n                title,\n                delete: deleteRecord,\n                deleteButtonLabel: deleteButtonLabel,\n            },\n            { onClose: _onClose }\n        );\n    }\n\n    let recordIsOpen = false;\n    return (params) => {\n        if (recordIsOpen) {\n            return;\n        }\n        recordIsOpen = true;\n\n        const onClose = params.onClose;\n        params = {\n            ...params,\n            onClose: (...args) => {\n                recordIsOpen = false;\n                if (onClose) {\n                    return onClose(...args);\n                }\n            },\n        };\n\n        try {\n            return openRecord(params);\n        } catch (e) {\n            recordIsOpen = false;\n            throw e;\n        }\n    };\n}\n\nexport function useX2ManyCrud(getList, isMany2Many) {\n    let saveRecord; // FIXME: isn't this \"createRecord\" instead?\n    if (isMany2Many) {\n        saveRecord = async (object) => {\n            const list = getList();\n            if (Array.isArray(object)) {\n                return list.addAndRemove({ add: object });\n            } else {\n                // object instanceof Record\n                await object.save({ reload: false });\n                return list.linkTo(object.resId);\n            }\n        };\n    } else {\n        saveRecord = async (record) => {\n            return getList().validateExtendedRecord(record);\n        };\n    }\n\n    const updateRecord = async (record) => {\n        if (isMany2Many) {\n            await record.save();\n        }\n        return getList().validateExtendedRecord(record);\n    };\n\n    const removeRecord = (record) => {\n        const list = getList();\n        if (isMany2Many) {\n            return list.forget(record);\n        }\n        return list.delete(record);\n    };\n\n    return {\n        saveRecord,\n        updateRecord,\n        removeRecord,\n    };\n}\n", "\nimport { Component } from \"@odoo/owl\";\nimport { evaluateExpr } from \"@web/core/py_js/py\";\nimport { formatDate, formatDateTime } from \"@web/core/l10n/dates\";\nimport { getClassNameFromDecoration } from \"@web/views/utils\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { DateTimeField } from \"../datetime/datetime_field\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nconst { DateTime } = luxon;\n\nexport class RemainingDaysField extends Component {\n    static components = { DateTimeField };\n\n    static props = {\n        ...standardFieldProps,\n        classes: { type: Object, optional: true },\n    };\n\n    static defaultProps = {\n        classes: {\n            'bf': 'days <= 0',\n            'danger': 'days < 0',\n            'warning': 'days == 0',\n        },\n    };\n\n    static template = \"web.RemainingDaysField\";\n\n    get diffDays() {\n        const { record, name } = this.props;\n        const value = record.data[name];\n        if (!value) {\n            return null;\n        }\n        const today = DateTime.local().startOf(\"day\");\n        const diff = value.startOf(\"day\").diff(today, \"days\");\n        return Math.floor(diff.days);\n    }\n\n    get diffString() {\n        if (this.diffDays === null) {\n            return \"\";\n        }\n        switch (this.diffDays) {\n            case -1:\n                return _t(\"Yesterday\");\n            case 0:\n                return _t(\"Today\");\n            case 1:\n                return _t(\"Tomorrow\");\n        }\n        if (Math.abs(this.diffDays) > 99) {\n            return this.formattedValue;\n        }\n        if (this.diffDays < 0) {\n            return _t(\"%s days ago\", -this.diffDays);\n        }\n        return _t(\"In %s days\", this.diffDays);\n    }\n\n    get formattedValue() {\n        const { record, name } = this.props;\n        return record.fields[name].type === \"datetime\"\n            ? formatDateTime(record.data[name], { format: localization.dateFormat })\n            : formatDate(record.data[name]);\n    }\n\n    get classNames() {\n        if (this.diffDays === null) {\n            return null;\n        }\n        if (!this.props.record.isActive) {\n            return null;\n        }\n        const classNames = {};\n        const evalContext = {days: this.diffDays, record: this.props.record.evalContext};\n        for (const decoration in this.props.classes) {\n            const value = evaluateExpr(this.props.classes[decoration], evalContext);\n            classNames[getClassNameFromDecoration(decoration)] = value;\n        }\n        return classNames;\n    }\n\n    get dateTimeFieldProps() {\n        return Object.fromEntries(\n            Object.entries(this.props).filter(([key]) => standardFieldProps[key])\n        );\n    }\n}\n\nexport const remainingDaysField = {\n    component: RemainingDaysField,\n    displayName: _t(\"Remaining Days\"),\n    supportedTypes: [\"date\", \"datetime\"],\n    extractProps: ({ options }) => {\n        return {\n            classes: options.classes,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"remaining_days\", remainingDaysField);\n", "import { registry } from \"@web/core/registry\";\nimport { SelectionField, selectionField } from \"@web/views/fields/selection/selection_field\";\n\n/**\n * The purpose of this field is to be able to define some values which should not be\n * displayed on our selection field, this way we can have multiple views for the same model\n * that uses different possible sets of values on the same selection field.\n */\nexport class FilterableSelectionField extends SelectionField {\n    static props = {\n        ...SelectionField.props,\n        whitelist_fname: { type: String, optional: true },\n        whitelisted_values: { type: Array, optional: true },\n        blacklisted_values: { type: Array, optional: true },\n    };\n\n    /**\n     * @override\n     */\n    get options() {\n        let options = super.options;\n        if (this.props.whitelist_fname) {\n            options = options.filter((option) => {\n                return (\n                    option[0] === this.props.record.data[this.props.name] ||\n                    this.props.record.data[this.props.whitelist_fname].includes(option[0])\n                );\n            });\n        } else if (this.props.whitelisted_values) {\n            options = options.filter((option) => {\n                return (\n                    option[0] === this.props.record.data[this.props.name] ||\n                    this.props.whitelisted_values.includes(option[0])\n                );\n            });\n        } else if (this.props.blacklisted_values) {\n            options = options.filter((option) => {\n                return (\n                    option[0] === this.props.record.data[this.props.name] ||\n                    !this.props.blacklisted_values.includes(option[0])\n                );\n            });\n        }\n        return options;\n    }\n}\n\nexport const filterableSelectionField = {\n    ...selectionField,\n    component: FilterableSelectionField,\n    supportedOptions: [\n        {\n            label: \"Whitelisted Values\",\n            name: \"whitelisted_values\",\n            type: \"string\",\n        },\n        {\n            label: \"Blacklisted Values\",\n            name: \"blacklisted_values\",\n            type: \"string\",\n        },\n        {\n            label: \"Whitelisted field name\",\n            name: \"whitelist_fname\",\n            type: \"string\",\n        },\n    ],\n    extractProps({ options }) {\n        const props = selectionField.extractProps(...arguments);\n        props.whitelist_fname = options.whitelist_fname;\n        props.whitelisted_values = options.whitelisted_values;\n        props.blacklisted_values = options.blacklisted_values;\n        return props;\n    },\n};\n\nregistry.category(\"fields\").add(\"filterable_selection\", filterableSelectionField);\n", "import { Component } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { getFieldDomain } from \"@web/model/relational_model/utils\";\nimport { useSpecialData } from \"@web/views/fields/relational_utils\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nexport class SelectionField extends Component {\n    static template = \"web.SelectionField\";\n    static props = {\n        ...standardFieldProps,\n        placeholder: { type: String, optional: true },\n        required: { type: Boolean, optional: true },\n        domain: { type: [Array, Function], optional: true },\n        autosave: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        autosave: false,\n    };\n\n    setup() {\n        this.type = this.props.record.fields[this.props.name].type;\n        if (this.type === \"many2one\") {\n            this.specialData = useSpecialData((orm, props) => {\n                const { relation } = props.record.fields[props.name];\n                const domain = getFieldDomain(props.record, props.name, props.domain);\n                return orm.call(relation, \"name_search\", [\"\", domain]);\n            });\n        }\n    }\n\n    get options() {\n        switch (this.type) {\n            case \"many2one\":\n                return [...this.specialData.data];\n            case \"selection\":\n                return this.props.record.fields[this.props.name].selection.filter(\n                    (option) => option[0] !== false && option[1] !== \"\"\n                );\n            default:\n                return [];\n        }\n    }\n    get string() {\n        switch (this.type) {\n            case \"many2one\":\n                return this.props.record.data[this.props.name]\n                    ? this.props.record.data[this.props.name][1]\n                    : \"\";\n            case \"selection\":\n                return this.props.record.data[this.props.name] !== false\n                    ? this.options.find((o) => o[0] === this.props.record.data[this.props.name])[1]\n                    : \"\";\n            default:\n                return \"\";\n        }\n    }\n    get value() {\n        const rawValue = this.props.record.data[this.props.name];\n        return this.type === \"many2one\" && rawValue ? rawValue[0] : rawValue;\n    }\n\n    stringify(value) {\n        return JSON.stringify(value);\n    }\n\n    /**\n     * @param {Event} ev\n     */\n    onChange(ev) {\n        const value = JSON.parse(ev.target.value);\n        switch (this.type) {\n            case \"many2one\":\n                if (value === false) {\n                    this.props.record.update(\n                        { [this.props.name]: false },\n                        { save: this.props.autosave }\n                    );\n                } else {\n                    this.props.record.update(\n                        {\n                            [this.props.name]: this.options.find((option) => option[0] === value),\n                        },\n                        { save: this.props.autosave }\n                    );\n                }\n                break;\n            case \"selection\":\n                this.props.record.update(\n                    { [this.props.name]: value },\n                    { save: this.props.autosave }\n                );\n                break;\n        }\n    }\n}\n\nexport const selectionField = {\n    component: SelectionField,\n    displayName: _t(\"Selection\"),\n    supportedTypes: [\"many2one\", \"selection\"],\n    isEmpty: (record, fieldName) => record.data[fieldName] === false,\n    extractProps({ attrs, viewType }, dynamicInfo) {\n        const props = {\n            autosave: viewType === \"kanban\",\n            placeholder: attrs.placeholder,\n            required: dynamicInfo.required,\n            domain: dynamicInfo.domain,\n        };\n        if (viewType === \"kanban\") {\n            props.readonly = dynamicInfo.readonly;\n        }\n        return props;\n    },\n};\n\nregistry.category(\"fields\").add(\"selection\", selectionField);\nregistry.category(\"fields\").add(\"kanban.selection\", selectionField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { SignatureDialog } from \"@web/core/signature/signature_dialog\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { imageUrl } from \"@web/core/utils/urls\";\nimport { isBinarySize } from \"@web/core/utils/binary\";\nimport { fileTypeMagicWordMap } from \"@web/views/fields/image/image_field\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nconst placeholder = \"/web/static/img/placeholder.png\";\n\nexport class SignatureField extends Component {\n    static template = \"web.SignatureField\";\n    static props = {\n        ...standardFieldProps,\n        defaultFont: { type: String },\n        fullName: { type: String, optional: true },\n        height: { type: Number, optional: true },\n        previewImage: { type: String, optional: true },\n        width: { type: Number, optional: true },\n        type: { validate: (t) => [\"initial\", \"signature\"].includes(t), optional: true },\n    };\n    static defaultProps = {\n        type: \"signature\",\n    };\n\n    setup() {\n        this.displaySignatureRatio = 3;\n\n        this.dialogService = useService(\"dialog\");\n        this.state = useState({\n            isValid: true,\n        });\n    }\n\n    get rawCacheKey() {\n        return this.props.record.data.write_date;\n    }\n\n    get getUrl() {\n        const { name, previewImage, record } = this.props;\n        if (this.state.isValid && this.value) {\n            if (isBinarySize(this.value)) {\n                return imageUrl(record.resModel, record.resId, previewImage || name, {\n                    unique: this.rawCacheKey,\n                });\n            } else {\n                // Use magic-word technique for detecting image type\n                const magic = fileTypeMagicWordMap[this.value[0]] || \"png\";\n                return `data:image/${magic};base64,${this.props.record.data[this.props.name]}`;\n            }\n        }\n        return placeholder;\n    }\n\n    get sizeStyle() {\n        let { width, height } = this.props;\n\n        if (!this.value) {\n            if (width && height) {\n                width = Math.min(width, this.displaySignatureRatio * height);\n                height = width / this.displaySignatureRatio;\n            } else if (width) {\n                height = width / this.displaySignatureRatio;\n            } else if (height) {\n                width = height * this.displaySignatureRatio;\n            }\n        }\n\n        let style = \"\";\n        if (width) {\n            style += `width:${width}px; max-width:${width}px;`;\n        }\n        if (height) {\n            style += `height:${height}px; max-height:${height}px;`;\n        }\n        return style;\n    }\n\n    get value() {\n        return this.props.record.data[this.props.name];\n    }\n\n    onClickSignature() {\n        if (!this.props.readonly) {\n            const nameAndSignatureProps = {\n                displaySignatureRatio: 3,\n                signatureType: this.props.type,\n                noInputName: true,\n            };\n            const { fullName, record } = this.props;\n            let defaultName = \"\";\n            if (fullName) {\n                let signName;\n                const fullNameData = record.data[fullName];\n                if (record.fields[fullName].type === \"many2one\") {\n                    // If m2o is empty, it will have falsy value in recordData\n                    signName = fullNameData && fullNameData[1];\n                } else {\n                    signName = fullNameData;\n                }\n                defaultName = signName === \"\" ? undefined : signName;\n            }\n\n            nameAndSignatureProps.defaultFont = this.props.defaultFont;\n\n            const dialogProps = {\n                defaultName,\n                nameAndSignatureProps,\n                uploadSignature: (signature) => this.uploadSignature(signature),\n            };\n            this.dialogService.add(SignatureDialog, dialogProps);\n        }\n    }\n\n    onLoadFailed() {\n        this.state.isValid = false;\n        this.notification.add(_t(\"Could not display the selected image\"), {\n            type: \"danger\",\n        });\n    }\n\n    /**\n     * Upload the signature image if valid and close the dialog.\n     *\n     * @private\n     */\n    uploadSignature({ signatureImage }) {\n        return this.props.record.update({\n            [this.props.name]: signatureImage.split(\",\")[1] || false,\n        });\n    }\n}\n\nexport const signatureField = {\n    component: SignatureField,\n    fieldDependencies: [{ name: \"write_date\", type: \"datetime\" }],\n    supportedOptions: [\n        {\n            label: _t(\"Prefill with\"),\n            name: \"full_name\",\n            type: \"field\",\n            availableTypes: [\"char\", \"many2one\"],\n            help: _t(\"The selected field will be used to pre-fill the signature\"),\n        },\n        {\n            label: _t(\"Default font\"),\n            name: \"default_font\",\n            type: \"string\",\n        },\n        {\n            label: _t(\"Size\"),\n            name: \"size\",\n            type: \"selection\",\n            choices: [\n                { label: _t(\"Small\"), value: \"[0,90]\" },\n                { label: _t(\"Medium\"), value: \"[0,180]\" },\n                { label: _t(\"Large\"), value: \"[0,270]\" },\n            ],\n        },\n        {\n            label: _t(\"Preview image field\"),\n            name: \"preview_image\",\n            type: \"field\",\n            availableTypes: [\"binary\"],\n        },\n    ],\n    extractProps: ({ attrs, options }) => ({\n        defaultFont: options.default_font || \"\",\n        fullName: options.full_name,\n        height: options.size ? options.size[1] || undefined : attrs.height,\n        previewImage: options.preview_image,\n        type: options.type,\n        width: options.size ? options.size[0] || undefined : attrs.width,\n    }),\n};\n\nregistry.category(\"fields\").add(\"signature\", signatureField);\n", "/**\n * @typedef StandardFieldProps\n * @property {string} [id]\n * @property {string} name\n * @property {boolean} [readonly]\n * @property {import(\"@web/model/relational_model/record\").Record} record\n */\n\nexport const standardFieldProps = {\n    id: { type: String, optional: true },\n    name: { type: String },\n    readonly: { type: Boolean, optional: true },\n    record: { type: Object },\n};\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component } from \"@odoo/owl\";\nconst formatters = registry.category(\"formatters\");\n\nexport class StatInfoField extends Component {\n    static template = \"web.StatInfoField\";\n    static props = {\n        ...standardFieldProps,\n        labelField: { type: String, optional: true },\n        noLabel: { type: Boolean, optional: true },\n        digits: { type: Array, optional: true },\n        string: { type: String, optional: true },\n    };\n\n    get formattedValue() {\n        const field = this.props.record.fields[this.props.name];\n        const formatter = formatters.get(field.type);\n        return formatter(this.props.record.data[this.props.name] || 0, {\n            digits: this.props.digits,\n            field,\n        });\n    }\n    get label() {\n        return this.props.labelField\n            ? this.props.record.data[this.props.labelField]\n            : this.props.string;\n    }\n}\n\nexport const statInfoField = {\n    component: StatInfoField,\n    displayName: _t(\"Stat Info\"),\n    supportedOptions: [\n        {\n            label: _t(\"Label field\"),\n            name: \"label_field\",\n            type: \"field\",\n            availableTypes: [\"char\"],\n        },\n    ],\n    supportedTypes: [\"float\", \"integer\", \"monetary\"],\n    isEmpty: () => false,\n    extractProps: ({ attrs, options, string }) => {\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\n        return {\n            digits,\n            labelField: options.label_field,\n            noLabel: exprToBoolean(attrs.nolabel),\n            string,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"statinfo\", statInfoField);\n", "import { Component } from \"@odoo/owl\";\nimport { useCommand } from \"@web/core/commands/command_hook\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { formatSelection } from \"../formatters\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nexport class StateSelectionField extends Component {\n    static template = \"web.StateSelectionField\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n    };\n    static props = {\n        ...standardFieldProps,\n        showLabel: { type: Boolean, optional: true },\n        withCommand: { type: Boolean, optional: true },\n        autosave: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        showLabel: true,\n    };\n\n    setup() {\n        this.colorPrefix = \"o_status_\";\n        this.colors = {\n            blocked: \"red\",\n            done: \"green\",\n        };\n        if (this.props.withCommand) {\n            const hotkeys = [\"D\", \"F\", \"G\"];\n            for (const [index, [value, label]] of this.options.entries()) {\n                useCommand(\n                    _t(\"Set kanban state as %s\", label),\n                    () => {\n                        this.updateRecord(value);\n                    },\n                    {\n                        category: \"smart_action\",\n                        hotkey: hotkeys[index] && \"alt+\" + hotkeys[index],\n                        isAvailable: () => this.props.record.data[this.props.name] !== value,\n                    }\n                );\n            }\n        }\n    }\n    get options() {\n        return this.props.record.fields[this.props.name].selection.map(([state, label]) => {\n            return [state, this.props.record.data[`legend_${state}`] || label];\n        });\n    }\n    get currentValue() {\n        return this.props.record.data[this.props.name] || this.options[0][0];\n    }\n    get label() {\n        if (\n            this.props.record.data[this.props.name] &&\n            this.props.record.data[`legend_${this.props.record.data[this.props.name][0]}`]\n        ) {\n            return this.props.record.data[`legend_${this.props.record.data[this.props.name][0]}`];\n        }\n        return formatSelection(this.currentValue, { selection: this.options });\n    }\n\n    statusColor(value) {\n        return this.colors[value] ? this.colorPrefix + this.colors[value] : \"\";\n    }\n\n    async updateRecord(value) {\n        await this.props.record.update({ [this.props.name]: value }, { save: this.props.autosave });\n    }\n}\n\nexport const stateSelectionField = {\n    component: StateSelectionField,\n    displayName: _t(\"Label Selection\"),\n    supportedOptions: [\n        {\n            label: _t(\"Autosave\"),\n            name: \"autosave\",\n            type: \"boolean\",\n            default: true,\n            help: _t(\n                \"If checked, the record will be saved immediately when the field is modified.\"\n            ),\n        },\n        {\n            label: _t(\"Hide label\"),\n            name: \"hide_label\",\n            type: \"boolean\",\n        },\n    ],\n    supportedTypes: [\"selection\"],\n    extractProps({ options, viewType }, dynamicInfo) {\n        return {\n            showLabel: 'hide_label' in options ? !options.hide_label : false,\n            withCommand: viewType === \"form\",\n            readonly: dynamicInfo.readonly,\n            autosave: \"autosave\" in options ? !!options.autosave : true,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"state_selection\", stateSelectionField);\n", "import { Component, onWillRender, useEffect, useExternalListener, useRef } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { useCommand } from \"@web/core/commands/command_hook\";\nimport { Domain } from \"@web/core/domain\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { groupBy } from \"@web/core/utils/arrays\";\nimport { escape } from \"@web/core/utils/strings\";\nimport { throttleForAnimation } from \"@web/core/utils/timing\";\nimport { getFieldDomain } from \"@web/model/relational_model/utils\";\nimport { useSpecialData } from \"@web/views/fields/relational_utils\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\n/**\n * @typedef {import(\"../standard_field_props\").StandardFieldProps & {\n *  domain?: [Array, Function];\n *  foldField?: string;\n *  isDisabled?: boolean;\n *  visibleSelection?: string[];\n *  withCommand?: boolean;\n * }} StatusBarFieldProps\n *\n * @typedef StatusBarItem\n * @property {number} value\n * @property {string} label\n * @property {boolean} isFolded\n * @property {boolean} isSelected\n *\n * @typedef StatusBarList\n * @property {string} label\n * @property {StatusBarItem[]} items\n */\n\n/**\n * @param {...HTMLElement} els\n */\nconst hide = (...els) => els.forEach((el) => el.classList.add(\"d-none\"));\n\n/**\n * @param {...HTMLElement} els\n */\nconst show = (...els) => els.forEach((el) => el.classList.remove(\"d-none\"));\n\n/** @extends {Component<StatusBarFieldProps>} */\nexport class StatusBarField extends Component {\n    static template = \"web.StatusBarField\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n    };\n    static props = {\n        ...standardFieldProps,\n        domain: { type: [Array, Function], optional: true },\n        foldField: { type: String, optional: true },\n        isDisabled: { type: Boolean, optional: true },\n        visibleSelection: { type: Array, element: String, optional: true },\n        withCommand: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        // Properties\n        this.items = {};\n        this.beforeRef = useRef(\"before\");\n        this.rootRef = useRef(\"root\");\n        this.afterRef = useRef(\"after\");\n        this.dropdownRef = useRef(\"dropdown\");\n\n        // Resize listeners\n        let status = \"idle\";\n        const adjust = () => {\n            status = \"adjusting\";\n            this.adjustVisibleItems();\n            this.render();\n            browser.requestAnimationFrame(() => (status = \"idle\"));\n        };\n\n        useEffect(\n            () => status === \"shouldAdjust\" && adjust(),\n            () => [status]\n        );\n\n        onWillRender(() => {\n            if (status !== \"adjusting\") {\n                Object.assign(this.items, this.getSortedItems());\n                status = \"shouldAdjust\";\n            }\n        });\n\n        useExternalListener(window, \"resize\", throttleForAnimation(adjust));\n\n        // Special data\n        if (this.field.type === \"many2one\") {\n            this.specialData = useSpecialData((orm, props) => {\n                const { foldField, name: fieldName, record } = props;\n                const { relation } = record.fields[fieldName];\n                const fieldNames = [\"display_name\"];\n                if (foldField) {\n                    fieldNames.push(foldField);\n                }\n                const value = record.data[fieldName];\n                let domain = getFieldDomain(record, fieldName, props.domain);\n                if (domain.length && value) {\n                    domain = Domain.or([[[\"id\", \"=\", value[0]]], domain]).toList(\n                        record.evalContext\n                    );\n                }\n                return orm.searchRead(relation, domain, fieldNames);\n            });\n        }\n\n        // Command palette\n        if (this.props.withCommand) {\n            const moveToCommandName = _t(\"Move to %s...\", escape(this.field.string));\n            useCommand(\n                moveToCommandName,\n                () => ({\n                    placeholder: moveToCommandName,\n                    providers: [\n                        {\n                            provide: () =>\n                                this.getAllItems().map((item) => ({\n                                    name: item.label,\n                                    action: () => this.selectItem(item),\n                                })),\n                        },\n                    ],\n                }),\n                {\n                    category: \"smart_action\",\n                    hotkey: \"alt+shift+x\",\n                    isAvailable: () => !this.props.isDisabled,\n                }\n            );\n            useCommand(\n                _t(\"Move to next %s\", this.field.string),\n                () => {\n                    const items = this.getAllItems();\n                    const nextIndex = items.findIndex((item) => item.isSelected) + 1;\n                    this.selectItem(items[nextIndex]);\n                },\n                {\n                    category: \"smart_action\",\n                    hotkey: \"alt+x\",\n                    isAvailable: () =>\n                        !this.props.isDisabled && !this.getAllItems().at(-1).isSelected,\n                }\n            );\n        }\n    }\n\n    /**\n     * @returns {{ selection?: [string, string][], string: string, type: \"many2one\" | \"selection\" }}\n     */\n    get field() {\n        return this.props.record.fields[this.props.name];\n    }\n\n    /**\n     * Determines what items must be visible and how they must be displayed.\n     * There are 4 main scenarios:\n     *\n     * 1. All items can be displayed inline, no modification in the UI;\n     *\n     * The following scenarios imply that the viewport is too small to display\n     * all items in one line. Adjustments are made incrementally:\n     *\n     * 2. Items up to 1 before the currently selected item are combined in a dropdown;\n     *\n     * 3. Items up to 1 after the currently selected item are combined in a dropdown,\n     * along with the initially folded items;\n     *\n     * 4. If that still doesn't suffice: all items are combined in a single dropdown.\n     */\n    adjustVisibleItems() {\n        // Get all visible buttons\n        const itemEls = [\n            ...this.rootRef.el.querySelectorAll(\".o_arrow_button:not(.dropdown-toggle)\"),\n        ];\n        const selectedIndex = itemEls.findIndex((el) =>\n            el.classList.contains(\"o_arrow_button_current\")\n        );\n        const itemsBefore = itemEls.slice(selectedIndex + 2).reverse();\n        const itemsAfter = itemEls.slice(0, Math.max(selectedIndex - 1, 0)).reverse();\n\n        // Reset hidden elements\n        show(...itemEls);\n        hide(this.dropdownRef.el, this.beforeRef.el);\n        if (this.items.folded.length) {\n            show(this.afterRef.el);\n            itemEls.forEach((el) => el.classList.remove(\"o_first\"));\n        } else {\n            hide(this.afterRef.el);\n            itemEls[0]?.classList.add(\"o_first\");\n        }\n\n        // Reset items variables\n        this.items.before = [];\n        this.items.after = [...this.items.folded];\n        const itemsToAssign = this.getAllItems().filter((item) => !item.isFolded);\n\n        while (this.areItemsWrapping()) {\n            if (itemsBefore.length) {\n                // Case 1: elements before can be hidden\n                show(this.beforeRef.el);\n                hide(itemsBefore.shift());\n                this.items.before.push(itemsToAssign.shift());\n            } else if (itemsAfter.length) {\n                // Case 2: elements before are hidden, elements after can be hidden\n                show(this.afterRef.el);\n                hide(itemsAfter.pop());\n                this.items.after.unshift(itemsToAssign.pop());\n            } else {\n                // Last resort: no elements can be hidden => fallback to single dropdown\n                show(this.dropdownRef.el);\n                hide(this.beforeRef.el, this.afterRef.el, ...itemEls);\n                break;\n            }\n        }\n    }\n\n    areItemsWrapping() {\n        const root = this.rootRef.el;\n        const firstItem = root.querySelector(\":scope > :not(.d-none)\");\n        if (!firstItem) {\n            return false;\n        }\n        const { height: currentHeight } = root.getBoundingClientRect();\n        const { height: targetHeight } = firstItem.getBoundingClientRect();\n        return currentHeight > targetHeight;\n    }\n\n    /**\n     * @returns {StatusBarItem[]}\n     */\n    getAllItems() {\n        const { foldField, name, record } = this.props;\n        const currentValue = record.data[name];\n        if (this.field.type === \"many2one\") {\n            // Many2one\n            return this.specialData.data.map((option) => ({\n                value: option.id,\n                label: option.display_name,\n                isFolded: option[foldField],\n                isSelected: Boolean(currentValue && option.id === currentValue[0]),\n            }));\n        } else {\n            // Selection\n            let { selection } = this.field;\n            const { visibleSelection } = this.props;\n            if (visibleSelection?.length) {\n                selection = selection.filter(\n                    ([value]) => value === currentValue || visibleSelection.includes(value)\n                );\n            }\n            return selection.map(([value, label]) => ({\n                value,\n                label,\n                isFolded: false,\n                isSelected: value === currentValue,\n            }));\n        }\n    }\n\n    getCurrentLabel() {\n        return this.getAllItems().find((item) => item.isSelected)?.label || _t(\"More\");\n    }\n\n    /**\n     * @param {StatusBarItem} item\n     */\n    getDropdownItemClassNames(item) {\n        const classNames = [];\n        if (item.isSelected) {\n            classNames.push(\"active\");\n        }\n        if (item.isSelected || this.props.isDisabled) {\n            classNames.push(\"disabled\");\n        }\n        return classNames.join(\" \");\n    }\n\n    /**\n     * @param {StatusBarItem} item\n     * TODO: unused, remove in master\n     */\n    getItemTooltip(item) {\n        if (item.isSelected) {\n            return _t(\"Current state\");\n        }\n        if (this.props.isDisabled) {\n            return _t(\"Not active state\");\n        }\n        return _t(\"Not active state, click to change it\");\n    }\n\n    getSortedItems() {\n        const before = [];\n        const after = [];\n        const { true: inline = [], false: folded = [] } = groupBy(\n            this.getAllItems(),\n            (item) => item.isSelected || !item.isFolded\n        );\n        inline.reverse(); // CSS rules account for this list to be reversed\n        after.push(...folded);\n        return { inline, before, after, folded };\n    }\n\n    /**\n     * @param {StatusBarItem} item\n     */\n    async selectItem(item) {\n        const { name, record } = this.props;\n        const value = this.field.type === \"many2one\" ? [item.value, item.label] : item.value;\n        await record.update({ [name]: value });\n        await record.save();\n    }\n\n    /**\n     * @param {CustomEvent<{ payload: StatusBarItem }>} ev\n     */\n    onDropdownItemSelected(ev) {\n        this.selectItem(ev.detail.payload);\n    }\n}\n\nexport const statusBarField = {\n    component: StatusBarField,\n    displayName: _t(\"Status\"),\n    supportedOptions: [\n        {\n            label: _t(\"Clickable\"),\n            name: \"clickable\",\n            type: \"boolean\",\n            default: true,\n        },\n        {\n            label: _t(\"Fold field\"),\n            name: \"fold_field\",\n            type: \"field\",\n            availableTypes: [\"boolean\"],\n            help: _t(\n                \"Boolean field from the model used in the relation, which indicates whether the state is folded or not.\"\n            ),\n        },\n    ],\n    supportedTypes: [\"many2one\", \"selection\"],\n    isEmpty: (record, fieldName) => !record.data[fieldName],\n    extractProps: ({ attrs, options, viewType }, dynamicInfo) => ({\n        isDisabled: !options.clickable || dynamicInfo.readonly,\n        visibleSelection: attrs.statusbar_visible?.trim().split(/\\s*,\\s*/g),\n        withCommand: viewType === \"form\",\n        foldField: options.fold_field,\n        domain: dynamicInfo.domain,\n    }),\n};\n\nregistry.category(\"fields\").add(\"statusbar\", statusBarField);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useAutoresize } from \"@web/core/utils/autoresize\";\nimport { useSpellCheck } from \"@web/core/utils/hooks\";\nimport { useDynamicPlaceholder } from \"../dynamic_placeholder_hook\";\nimport { useInputField } from \"../input_field_hook\";\nimport { parseInteger } from \"../parsers\";\nimport { standardFieldProps } from \"../standard_field_props\";\nimport { TranslationButton } from \"../translation_button\";\n\nimport { Component, useExternalListener, useEffect, useRef } from \"@odoo/owl\";\n\nexport class TextField extends Component {\n    static template = \"web.TextField\";\n    static components = {\n        TranslationButton,\n    };\n    static props = {\n        ...standardFieldProps,\n        lineBreaks: { type: Boolean, optional: true },\n        placeholder: { type: String, optional: true },\n        dynamicPlaceholder: { type: Boolean, optional: true },\n        dynamicPlaceholderModelReferenceField: { type: String, optional: true },\n        rowCount: { type: Number, optional: true },\n    };\n    static defaultProps = {\n        lineBreaks: true,\n        dynamicPlaceholder: false,\n        rowCount: 2,\n    };\n\n    setup() {\n        this.divRef = useRef(\"div\");\n        this.textareaRef = useRef(\"textarea\");\n        if (this.props.dynamicPlaceholder) {\n            this.dynamicPlaceholder = useDynamicPlaceholder(this.textareaRef);\n            useExternalListener(document, \"keydown\", this.dynamicPlaceholder.onKeydown);\n            useEffect(() =>\n                this.dynamicPlaceholder.updateModel(\n                    this.props.dynamicPlaceholderModelReferenceField\n                )\n            );\n        }\n        useInputField({\n            getValue: () => this.props.record.data[this.props.name] || \"\",\n            refName: \"textarea\",\n            preventLineBreaks: !this.props.lineBreaks,\n        });\n        useSpellCheck({ refName: \"textarea\" });\n\n        useAutoresize(this.textareaRef, { minimumHeight: this.minimumHeight });\n\n        this.selectionStart = this.props.record.data[this.props.name]?.length || 0;\n    }\n\n    async onBlur() {\n        this.selectionStart = this.textareaRef.el.selectionStart;\n    }\n\n    async onDynamicPlaceholderOpen() {\n        await this.dynamicPlaceholder.open({\n            validateCallback: this.onDynamicPlaceholderValidate.bind(this),\n        });\n    }\n\n    get isTranslatable() {\n        return this.props.record.fields[this.props.name].translate;\n    }\n    get minimumHeight() {\n        return this.props.lineBreaks ? 50 : 0;\n    }\n    get rowCount() {\n        return this.props.lineBreaks ? this.props.rowCount : 1;\n    }\n\n    async onDynamicPlaceholderValidate(chain, defaultValue) {\n        if (chain) {\n            this.textareaRef.el.focus();\n            const dynamicPlaceholder = ` {{object.${chain}${\n                defaultValue?.length ? ` ||| ${defaultValue}` : \"\"\n            }}}`;\n            this.textareaRef.el.setRangeText(\n                dynamicPlaceholder,\n                this.selectionStart,\n                this.selectionStart,\n                \"end\"\n            );\n            // trigger events to make the field dirty\n            this.textareaRef.el.dispatchEvent(new InputEvent(\"input\"));\n            this.textareaRef.el.dispatchEvent(new KeyboardEvent(\"keydown\"));\n            this.textareaRef.el.focus();\n        }\n    }\n}\n\nexport const textField = {\n    component: TextField,\n    displayName: _t(\"Multiline Text\"),\n    supportedOptions: [\n        {\n            label: _t(\"Enable line breaks\"),\n            name: \"line_breaks\",\n            type: \"boolean\",\n            default: true,\n        },\n    ],\n    supportedTypes: [\"html\", \"text\"],\n    extractProps: ({ attrs, options }) => ({\n        placeholder: attrs.placeholder,\n        dynamicPlaceholder: options?.dynamic_placeholder || false,\n        dynamicPlaceholderModelReferenceField:\n            options?.dynamic_placeholder_model_reference_field || \"\",\n        rowCount: attrs.rows && parseInteger(attrs.rows),\n        lineBreaks: options?.line_breaks !== undefined ? Boolean(options.line_breaks) : true,\n    }),\n};\n\nregistry.category(\"fields\").add(\"text\", textField);\n\nexport class ListTextField extends TextField {\n    static defaultProps = {\n        ...super.defaultProps,\n        rowCount: 1,\n    };\n\n    get minimumHeight() {\n        return 0;\n    }\n    get rowCount() {\n        return this.props.rowCount;\n    }\n}\n\nexport const listTextField = {\n    ...textField,\n    component: ListTextField,\n};\n\nregistry.category(\"fields\").add(\"list.text\", listTextField);\n", "import { formatDateTime } from \"@web/core/l10n/dates\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { selectionField, SelectionField } from \"../selection/selection_field\";\n\nconst { DateTime } = luxon;\n\nexport class TimezoneMismatchField extends SelectionField {\n    static template = \"web.TimezoneMismatchField\";\n    static props = {\n        ...super.props,\n        tzOffsetField: { type: String, optional: true },\n        mismatchTitle: { type: String, optional: true },\n    };\n    static defaultProps = {\n        ...super.defaultProps,\n        tzOffsetField: \"tz_offset\",\n        mismatchTitle: _t(\n            \"Timezone Mismatch : This timezone is different from that of your browser.\\nPlease, set the same timezone as your browser's to avoid time discrepancies in your system.\"\n        ),\n    };\n\n    get mismatch() {\n        const userOffset = this.props.record.data[this.props.tzOffsetField];\n        if (userOffset && this.props.record.data[this.props.name]) {\n            const offset = -new Date().getTimezoneOffset();\n            let browserOffset = offset < 0 ? \"-\" : \"+\";\n            browserOffset += Math.abs(offset / 60)\n                .toFixed(0)\n                .padStart(2, \"0\");\n            browserOffset += Math.abs(offset % 60)\n                .toFixed(0)\n                .padStart(2, \"0\");\n            return browserOffset !== userOffset;\n        } else if (!this.props.record.data[this.props.name]) {\n            return true;\n        }\n        return false;\n    }\n    get mismatchTitle() {\n        if (!this.props.record.data[this.props.name]) {\n            return _t(\"Set a timezone on your user\");\n        }\n        return this.props.mismatchTitle;\n    }\n    get options() {\n        if (!this.mismatch) {\n            return super.options;\n        }\n        return super.options.map((option) => {\n            const [value, label] = option;\n            if (value === this.props.record.data[this.props.name]) {\n                const offset = this.props.record.data[this.props.tzOffsetField].match(\n                    /([+-])([0-9]{2})([0-9]{2})/\n                );\n                const sign = offset[1] === \"-\" ? -1 : 1;\n                const userOffset = sign * (parseInt(offset[2]) * 60 + parseInt(offset[3]));\n                const browserOffset = -new Date().getTimezoneOffset();\n                // UTC time of the user's selected timezone.\n                // E.g.\n                // - current time in UTC, say equal to 2021-01-01T00:00:00Z\n                // - userOffset of +0300 = 180 minutes\n                // - browserOffset of +0200 = -new Date().getTimezoneOffset() = 120 minutes\n                // - userUTCDatetime is then 2021-01-01T01:00:00Z\n                const userUTCDatetime = DateTime.utc().plus({\n                    minutes: userOffset - browserOffset,\n                });\n                return [value, `${label} (${formatDateTime(userUTCDatetime)})`];\n            }\n            return [value, label];\n        });\n    }\n}\n\nexport const timezoneMismatchField = {\n    ...selectionField,\n    component: TimezoneMismatchField,\n    additionalClasses: [\"d-flex\"],\n    supportedOptions: [\n        ...(selectionField.supportedOptions || []),\n        {\n            label: _t(\"Mismatch title\"),\n            name: \"mismatch_title\",\n            type: \"string\",\n        },\n        {\n            label: _t(\"Timezone offset field\"),\n            name: \"tz_offset_field\",\n            type: \"field\",\n            availableTypes: [\"char\"],\n        },\n    ],\n    extractProps({ options }) {\n        const props = selectionField.extractProps(...arguments);\n        props.tzOffsetField = options.tz_offset_field;\n        props.mismatchTitle = options.mismatch_title;\n        return props;\n    },\n};\n\nregistry.category(\"fields\").add(\"timezone_mismatch\", timezoneMismatchField);\n", "import { localization } from \"@web/core/l10n/localization\";\nimport { useOwnedDialogs } from \"@web/core/utils/hooks\";\nimport { user } from \"@web/core/user\";\nimport { TranslationDialog } from \"./translation_dialog\";\n\nimport { Component } from \"@odoo/owl\";\n\n/**\n * Prepares a function that will open the dialog that allows to edit translation\n * values for a given field.\n *\n * It is mainly a factorization of the feature that is also used\n * in legacy_fields. We expect it to be fully implemented in TranslationButton\n * when legacy code is removed.\n */\nexport function useTranslationDialog() {\n    const addDialog = useOwnedDialogs();\n\n    async function openTranslationDialog({ record, fieldName }) {\n        const saved = await record.save();\n        if (!saved) {\n            return;\n        }\n        const { resModel, resId } = record;\n\n        addDialog(TranslationDialog, {\n            fieldName: fieldName,\n            resId: resId,\n            resModel: resModel,\n            userLanguageValue: record.data[fieldName] || \"\",\n            isComingFromTranslationAlert: false,\n            onSave: async () => {\n                await record.load();\n            },\n        });\n    }\n\n    return openTranslationDialog;\n}\n\nexport class TranslationButton extends Component {\n    static template = \"web.TranslationButton\";\n    static props = {\n        fieldName: { type: String },\n        record: { type: Object },\n    };\n\n    setup() {\n        this.translationDialog = useTranslationDialog();\n    }\n\n    get isMultiLang() {\n        return localization.multiLang;\n    }\n    get lang() {\n        return new Intl.Locale(user.lang).language.toUpperCase();\n    }\n\n    onClick() {\n        const { fieldName, record } = this.props;\n        this.translationDialog({ fieldName, record });\n    }\n}\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { loadLanguages, _t } from \"@web/core/l10n/translation\";\nimport { jsToPyLocale } from \"@web/core/l10n/utils\";\n\nimport { Component, onWillStart } from \"@odoo/owl\";\n\nexport class TranslationDialog extends Component {\n    static template = \"web.TranslationDialog\";\n    static components = { Dialog };\n    static props = {\n        fieldName: String,\n        resId: Number,\n        resModel: String,\n        userLanguageValue: { type: String, optional: true },\n        isComingFromTranslationAlert: { type: Boolean, optional: true },\n        onSave: Function,\n        close: Function,\n        isText: { type: Boolean, optional: true },\n        showSource: { type: Boolean, optional: true },\n    };\n    setup() {\n        super.setup();\n        this.title = _t(\"Translate: %s\", this.props.fieldName);\n\n        this.user = user;\n        this.orm = useService(\"orm\");\n\n        this.terms = [];\n        this.updatedTerms = {};\n\n        onWillStart(async () => {\n            const languages = await loadLanguages(this.orm);\n            const [translations, context] = await this.loadTranslations(languages);\n            let id = 1;\n            translations.forEach((t) => (t.id = id++));\n            this.props.isText = context.translation_type === \"text\";\n            this.props.showSource = context.translation_show_source;\n\n            this.terms = translations.map((term) => {\n                const relatedLanguage = languages.find((l) => l[0] === term.lang);\n                const termInfo = {\n                    ...term,\n                    langName: relatedLanguage[1],\n                    value: term.value || \"\",\n                };\n                // we set the translation value coming from the database, except for the language\n                // the user is currently utilizing. Then we set the translation value coming\n                // from the value of the field in the form\n                if (\n                    term.lang === jsToPyLocale(user.lang) &&\n                    !this.props.showSource &&\n                    !this.props.isComingFromTranslationAlert\n                ) {\n                    this.updatedTerms[term.id] = this.props.userLanguageValue;\n                    termInfo.value = this.props.userLanguageValue;\n                }\n                return termInfo;\n            });\n            this.terms.sort((a, b) => a.langName.localeCompare(b.langName));\n        });\n    }\n\n    get domain() {\n        const domain = this.props.domain;\n        if (this.props.searchName) {\n            domain.push([\"name\", \"=\", `${this.props.searchName}`]);\n        }\n        return domain;\n    }\n\n    /**\n     * Load the translation terms for the installed language, for the current model and res_id\n     */\n    async loadTranslations(languages) {\n        return this.orm.call(this.props.resModel, \"get_field_translations\", [\n            [this.props.resId],\n            this.props.fieldName,\n        ]);\n    }\n\n    /**\n     * Save all the terms that have been updated\n     */\n    async onSave() {\n        const translations = {};\n\n        this.terms.map((term) => {\n            const updatedTermValue = this.updatedTerms[term.id];\n            if (term.id in this.updatedTerms && term.value !== updatedTermValue) {\n                if (this.props.showSource) {\n                    if (!translations[term.lang]) {\n                        translations[term.lang] = {};\n                    }\n                    translations[term.lang][term.source] = updatedTermValue || term.source;\n                } else {\n                    translations[term.lang] = updatedTermValue || false;\n                }\n            }\n        });\n\n        await this.orm.call(this.props.resModel, \"update_field_translations\", [\n            [this.props.resId],\n            this.props.fieldName,\n            translations,\n        ]);\n\n        await this.props.onSave();\n        this.props.close();\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useInputField } from \"../input_field_hook\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class UrlField extends Component {\n    static template = \"web.UrlField\";\n    static props = {\n        ...standardFieldProps,\n        placeholder: { type: String, optional: true },\n        text: { type: String, optional: true },\n        websitePath: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        useInputField({ getValue: () => this.value });\n    }\n\n    get value() {\n        return this.props.record.data[this.props.name] || \"\";\n    }\n\n    get formattedHref() {\n        let value = this.props.record.data[this.props.name];\n        if (value && !this.props.websitePath) {\n            const regex = /^((ftp|http)s?:\\/)?\\//i; // http(s)://... ftp(s)://... /...\n            value = !regex.test(value) ? `http://${value}` : value;\n        }\n        return value;\n    }\n}\n\nexport const urlField = {\n    component: UrlField,\n    displayName: _t(\"URL\"),\n    supportedOptions: [\n        {\n            label: _t(\"Is a website path\"),\n            name: \"website_path\",\n            type: \"boolean\",\n            help: _t(\"If True, the url will be used as it is, without any prefix added to it.\"),\n        },\n    ],\n    supportedTypes: [\"char\"],\n    extractProps: ({ attrs, options }) => ({\n        text: attrs.text,\n        websitePath: options.website_path,\n        placeholder: attrs.placeholder,\n    }),\n};\n\nregistry.category(\"fields\").add(\"url\", urlField);\n\nclass FormUrlField extends UrlField {\n    static template = \"web.FormUrlField\";\n}\n\nexport const formUrlField = {\n    ...urlField,\n    component: FormUrlField,\n};\n\nregistry.category(\"fields\").add(\"form.url\", formUrlField);\n", "import { registry } from \"@web/core/registry\";\nimport { formatX2many } from \"../formatters\";\nimport { standardFieldProps } from \"../standard_field_props\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class ListX2ManyField extends Component {\n    static template = \"web.ListX2ManyField\";\n    static props = { ...standardFieldProps };\n\n    get formattedValue() {\n        return formatX2many(this.props.record.data[this.props.name]);\n    }\n}\n\nexport const listX2ManyField = {\n    component: ListX2ManyField,\n    useSubView: false,\n};\n\nregistry.category(\"fields\").add(\"list.one2many\", listX2ManyField);\nregistry.category(\"fields\").add(\"list.many2many\", listX2ManyField);\n", "import { makeContext } from \"@web/core/context\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Pager } from \"@web/core/pager/pager\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { getFieldDomain } from \"@web/model/relational_model/utils\";\nimport {\n    useActiveActions,\n    useAddInlineRecord,\n    useOpenX2ManyRecord,\n    useSelectCreate,\n    useX2ManyCrud,\n} from \"@web/views/fields/relational_utils\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { KanbanRenderer } from \"@web/views/kanban/kanban_renderer\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { computeViewClassName } from \"@web/views/utils\";\nimport { ViewButton } from \"@web/views/view_button/view_button\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class X2ManyField extends Component {\n    static template = \"web.X2ManyField\";\n    static components = { Pager, KanbanRenderer, ListRenderer, ViewButton };\n    static props = {\n        ...standardFieldProps,\n        addLabel: { type: String, optional: true },\n        editable: { type: String, optional: true },\n        viewMode: { type: String, optional: true },\n        widget: { type: String, optional: true },\n        crudOptions: { type: Object, optional: true },\n        string: { type: String, optional: true },\n        relatedFields: { type: Object, optional: true },\n        views: { type: Object, optional: true },\n        domain: { type: [Array, Function], optional: true },\n        context: { type: Object },\n    };\n\n    setup() {\n        this.field = this.props.record.fields[this.props.name];\n        const { saveRecord, updateRecord, removeRecord } = useX2ManyCrud(\n            () => this.list,\n            this.isMany2Many\n        );\n\n        this.archInfo = this.props.views?.[this.props.viewMode] || {};\n        const classes = this.props.viewMode\n            ? [\"o_field_x2many\", `o_field_x2many_${this.props.viewMode}`]\n            : [\"o_field_x2many\"];\n        this.className = computeViewClassName(this.props.viewMode, this.archInfo.xmlDoc, classes);\n\n        const { activeActions, creates } = this.archInfo;\n        if (this.props.viewMode === \"kanban\") {\n            this.creates = creates.length\n                ? creates\n                : [\n                      {\n                          type: \"create\",\n                          string: this.props.addLabel || _t(\"Add\"),\n                          class: \"o-kanban-button-new\",\n                      },\n                  ];\n        }\n        const subViewActiveActions = activeActions;\n        this.activeActions = useActiveActions({\n            crudOptions: Object.assign({}, this.props.crudOptions, {\n                onDelete: removeRecord,\n                edit: this.props.record.isInEdition,\n            }),\n            fieldType: this.isMany2Many ? \"many2many\" : \"one2many\",\n            subViewActiveActions,\n            getEvalParams: (props) => {\n                return {\n                    evalContext: props.record.evalContext,\n                    readonly: props.readonly,\n                };\n            },\n        });\n\n        this.addInLine = useAddInlineRecord({\n            addNew: (...args) => this.list.addNewRecord(...args),\n        });\n\n        const openRecord = useOpenX2ManyRecord({\n            resModel: this.list.resModel,\n            activeField: this.activeField,\n            activeActions: this.activeActions,\n            getList: () => this.list,\n            saveRecord,\n            updateRecord,\n            isMany2Many: this.isMany2Many,\n        });\n        this._openRecord = (params) => {\n            const activeElement = document.activeElement;\n            openRecord({\n                ...params,\n                onClose: () => {\n                    if (activeElement) {\n                        activeElement.focus();\n                    }\n                },\n            });\n        };\n        this.canOpenRecord =\n            this.props.viewMode === \"list\"\n                ? !(this.archInfo.editable || this.props.editable)\n                : true;\n\n        const selectCreate = useSelectCreate({\n            resModel: this.props.record.data[this.props.name].resModel,\n            activeActions: this.activeActions,\n            onSelected: (resIds) => saveRecord(resIds),\n            onCreateEdit: ({ context }) => this._openRecord({ context }),\n            onUnselect: this.isMany2Many ? undefined : () => saveRecord(),\n        });\n\n        this.selectCreate = (params) => {\n            const p = Object.assign({}, params);\n            const currentIds = this.props.record.data[this.props.name].currentIds.filter(\n                (id) => typeof id === \"number\"\n            );\n            p.domain = [...(p.domain || []), \"!\", [\"id\", \"in\", currentIds]];\n            return selectCreate(p);\n        };\n        this.action = useService(\"action\");\n    }\n\n    get activeField() {\n        return {\n            fields: this.props.relatedFields,\n            views: this.props.views,\n            viewMode: this.props.viewMode,\n            string: this.props.string,\n        };\n    }\n\n    get displayControlPanelButtons() {\n        return (\n            this.props.viewMode === \"kanban\" &&\n            (\"link\" in this.activeActions ? this.activeActions.link : this.activeActions.create) &&\n            !this.props.readonly\n        );\n    }\n\n    get isMany2Many() {\n        return this.field.type === \"many2many\" || this.props.widget === \"many2many\";\n    }\n\n    get list() {\n        return this.props.record.data[this.props.name];\n    }\n\n    get nestedKeyOptionalFieldsData() {\n        return {\n            field: this.props.name,\n            model: this.props.record.resModel,\n            viewMode: \"form\",\n        };\n    }\n\n    get pagerProps() {\n        const list = this.list;\n        return {\n            offset: list.offset,\n            limit: list.limit,\n            total: list.count,\n            onUpdate: async ({ offset, limit }) => {\n                const initialLimit = this.list.limit;\n                const leaved = await list.leaveEditMode();\n                if (leaved) {\n                    if (initialLimit === limit && initialLimit === this.list.limit + 1) {\n                        // Unselecting the edited record might have abandonned it. If the page\n                        // size was reached before that record was created, the limit was temporarily\n                        // increased to keep that new record in the current page, and abandonning it\n                        // decreased this limit back to it's initial value, so we keep this into\n                        // account in the offset/limit update we're about to do.\n                        offset -= 1;\n                        limit -= 1;\n                    }\n                    await list.load({ limit, offset });\n                    this.render();\n                }\n            },\n            withAccessKey: false,\n        };\n    }\n\n    get rendererProps() {\n        const { archInfo } = this;\n        const props = {\n            archInfo,\n            list: this.list,\n            openRecord: this.openRecord.bind(this),\n        };\n\n        if (this.props.viewMode === \"kanban\") {\n            const recordsDraggable = !this.props.readonly && archInfo.recordsDraggable;\n            props.archInfo = { ...archInfo, recordsDraggable };\n            props.readonly = this.props.readonly;\n            // TODO: apply same logic in the list case\n            props.deleteRecord = (record) => {\n                if (this.isMany2Many) {\n                    return this.list.forget(record);\n                }\n                return this.list.delete(record);\n            };\n            return props;\n        }\n\n        const editable =\n            (this.archInfo.activeActions.edit && archInfo.editable) || this.props.editable;\n        props.activeActions = this.activeActions;\n        props.cycleOnTab = false;\n        props.editable = !this.props.readonly && editable;\n        props.nestedKeyOptionalFieldsData = this.nestedKeyOptionalFieldsData;\n        props.onAdd = (params) => {\n            params.editable =\n                !this.props.readonly && (\"editable\" in params ? params.editable : editable);\n            this.onAdd(params);\n        };\n        props.onOpenFormView = this.switchToForm.bind(this);\n        props.hasOpenFormViewButton = archInfo.editable ? archInfo.openFormView : false;\n        return props;\n    }\n\n    async switchToForm(record) {\n        await this.props.record.save();\n        this.action.doAction(\n            {\n                type: \"ir.actions.act_window\",\n                views: [[false, \"form\"]],\n                res_id: record.resId,\n                res_model: this.list.resModel,\n            },\n            {\n                props: { resIds: this.list.resIds },\n            }\n        );\n    }\n\n    async onAdd({ context, editable } = {}) {\n        context = makeContext([this.props.context, context]);\n        if (this.isMany2Many) {\n            const domain = getFieldDomain(this.props.record, this.props.name, this.props.domain);\n            const { string } = this.props;\n            const title = _t(\"Add: %s\", string);\n            return this.selectCreate({ domain, context, title });\n        }\n        if (editable) {\n            const editedRecord = this.list.editedRecord;\n            if (editedRecord) {\n                const proms = [];\n                this.list.model.bus.trigger(\"NEED_LOCAL_CHANGES\", { proms });\n                await Promise.all([...proms, editedRecord._updatePromise]);\n                await this.list.leaveEditMode({ canAbandon: false });\n            }\n            if (!this.list.editedRecord) {\n                return this.addInLine({ context, editable });\n            }\n            return;\n        }\n        return this._openRecord({ context });\n    }\n\n    async openRecord(record) {\n        if (this.canOpenRecord) {\n            return this._openRecord({\n                record,\n                context: this.props.context,\n                mode: this.props.readonly ? \"readonly\" : \"edit\",\n            });\n        }\n    }\n}\n\nexport const x2ManyField = {\n    component: X2ManyField,\n    displayName: _t(\"Relational table\"),\n    supportedTypes: [\"one2many\", \"many2many\"],\n    useSubView: true,\n    extractProps: (\n        { attrs, relatedFields, viewMode, views, widget, options, string },\n        dynamicInfo\n    ) => {\n        const props = {\n            addLabel: attrs[\"add-label\"],\n            context: dynamicInfo.context,\n            domain: dynamicInfo.domain,\n            crudOptions: options,\n            string,\n        };\n        if (viewMode) {\n            props.views = views;\n            props.viewMode = viewMode;\n            props.relatedFields = relatedFields;\n        }\n        if (widget) {\n            props.widget = widget;\n        }\n        return props;\n    },\n};\n\nregistry.category(\"fields\").add(\"one2many\", x2ManyField);\nregistry.category(\"fields\").add(\"many2many\", x2ManyField);\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\n\nimport { Component, onWillRender } from \"@odoo/owl\";\nexport class ButtonBox extends Component {\n    static template = \"web.Form.ButtonBox\";\n    static components = { Dropdown, DropdownItem };\n    static props = {\n        slots: Object,\n        class: { type: String, optional: true },\n    };\n    static defaultProps = {\n        class: \"\",\n    };\n\n    setup() {\n        const ui = useService(\"ui\");\n        onWillRender(() => {\n            const maxVisibleButtons = [0, 0, 0, 7, 4, 5, 8][ui.size] ?? 8;\n            const allVisibleButtons = Object.entries(this.props.slots)\n                .filter(([_, slot]) => this.isSlotVisible(slot))\n                .map(([slotName]) => slotName);\n            if (allVisibleButtons.length <= maxVisibleButtons) {\n                this.visibleButtons = allVisibleButtons;\n                this.additionalButtons = [];\n                this.isFull = allVisibleButtons.length === maxVisibleButtons;\n            } else {\n                // -1 for \"More\" dropdown\n                const splitIndex = Math.max(maxVisibleButtons - 1, 0);\n                this.visibleButtons = allVisibleButtons.slice(0, splitIndex);\n                this.additionalButtons = allVisibleButtons.slice(splitIndex);\n                this.isFull = true;\n            }\n        });\n    }\n\n    isSlotVisible(slot) {\n        return !(\"isVisible\" in slot) || slot.isVisible;\n    }\n}\n", "import { exprToBoolean } from \"@web/core/utils/strings\";\nimport { visitXML } from \"@web/core/utils/xml\";\nimport { Field } from \"@web/views/fields/field\";\nimport { getActiveActions } from \"@web/views/utils\";\nimport { Widget } from \"@web/views/widgets/widget\";\n\nexport class FormArchParser {\n    parse(xmlDoc, models, modelName) {\n        const jsClass = xmlDoc.getAttribute(\"js_class\");\n        const disableAutofocus = exprToBoolean(xmlDoc.getAttribute(\"disable_autofocus\") || \"\");\n        const activeActions = getActiveActions(xmlDoc);\n        const fieldNodes = {};\n        const widgetNodes = {};\n        let widgetNextId = 0;\n        const fieldNextIds = {};\n        let autofocusFieldId = null;\n        visitXML(xmlDoc, (node) => {\n            if (node.tagName === \"field\") {\n                const fieldInfo = Field.parseFieldNode(node, models, modelName, \"form\", jsClass);\n                if (!(fieldInfo.name in fieldNextIds)) {\n                    fieldNextIds[fieldInfo.name] = 0;\n                }\n                const fieldId = `${fieldInfo.name}_${fieldNextIds[fieldInfo.name]++}`;\n                fieldNodes[fieldId] = fieldInfo;\n                node.setAttribute(\"field_id\", fieldId);\n                if (exprToBoolean(node.getAttribute(\"default_focus\") || \"\")) {\n                    autofocusFieldId = fieldId;\n                }\n                if (fieldInfo.type === \"properties\") {\n                    activeActions.addPropertyFieldValue = true;\n                }\n                return false;\n            } else if (node.tagName === \"widget\") {\n                const widgetInfo = Widget.parseWidgetNode(node);\n                const widgetId = `widget_${++widgetNextId}`;\n                widgetNodes[widgetId] = widgetInfo;\n                node.setAttribute(\"widget_id\", widgetId);\n            }\n        });\n        return {\n            activeActions,\n            autofocusFieldId,\n            disableAutofocus,\n            fieldNodes,\n            widgetNodes,\n            xmlDoc,\n        };\n    }\n}\n", "import { CogMenu } from \"@web/search/cog_menu/cog_menu\";\nimport { StatusBarDropdownItems } from \"../status_bar_dropdown_items/status_bar_dropdown_items\";\n\nexport class FormCogMenu extends CogMenu {\n    static template = \"web.FormCogMenu\";\n    static components = {\n        ...CogMenu.components,\n        StatusBarDropdownItems,\n    };\n    static props = {\n        ...CogMenu.props,\n        slots: { type: Object, optional: true },\n    };\n}\n", "import { registry } from \"@web/core/registry\";\nimport { SIZES } from \"@web/core/ui/ui_service\";\nimport {\n    append,\n    combineAttributes,\n    createElement,\n    createTextNode,\n    getTag,\n} from \"@web/core/utils/xml\";\nimport { toStringExpression } from \"@web/views/utils\";\nimport {\n    copyAttributes,\n    getModifier,\n    isComponentNode,\n    isTextNode,\n    makeSeparator,\n} from \"@web/views/view_compiler\";\nimport { ViewCompiler } from \"../view_compiler\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\n\nconst compilersRegistry = registry.category(\"form_compilers\");\n\nfunction appendAttf(el, attr, string) {\n    const attrKey = `t-attf-${attr}`;\n    const attrVal = el.getAttribute(attrKey);\n    el.setAttribute(attrKey, appendToExpr(attrVal, string));\n}\n\nfunction appendToExpr(expr, string) {\n    const re = /{{.*}}/;\n    const oldString = re.exec(expr);\n    return oldString ? `${oldString} {{${string} }}` : `{{${string} }}`;\n}\n\n/**\n * @param {Record<string, any>} obj\n * @returns {string}\n */\nexport function objectToString(obj) {\n    return `{${Object.entries(obj)\n        .map((t) => t.join(\":\"))\n        .join(\",\")}}`;\n}\n\nexport class FormCompiler extends ViewCompiler {\n    setup() {\n        this.encounteredFields = {};\n        /** @type {Record<string, Element[]>} */\n        this.labels = {};\n        this.noteBookId = 0;\n        this.compilers.push(\n            ...compilersRegistry.getAll(),\n            { selector: \"div[name='button_box']\", fn: this.compileButtonBox },\n            { selector: \"footer\", fn: this.compileFooter },\n            { selector: \"form\", fn: this.compileForm, doNotCopyAttributes: true },\n            { selector: \"group\", fn: this.compileGroup },\n            { selector: \"header\", fn: this.compileHeader },\n            { selector: \"label\", fn: this.compileLabel, doNotCopyAttributes: true },\n            { selector: \"notebook\", fn: this.compileNotebook },\n            { selector: \"setting\", fn: this.compileSetting },\n            { selector: \"separator\", fn: this.compileSeparator },\n            { selector: \"sheet\", fn: this.compileSheet }\n        );\n    }\n\n    compile(key, params = {}) {\n        const compiled = super.compile(...arguments);\n        if (!params.isSubView) {\n            compiled.children[0].setAttribute(\"t-ref\", \"compiled_view_root\");\n        }\n        return compiled;\n    }\n\n    createLabelFromField(fieldId, fieldName, fieldString, label, params) {\n        let labelText = label.textContent || fieldString;\n        if (label.hasAttribute(\"data-no-label\")) {\n            labelText = toStringExpression(\"\");\n        } else {\n            labelText = labelText\n                ? toStringExpression(labelText)\n                : `__comp__.props.record.fields['${fieldName}'].string`;\n        }\n        const formLabel = createElement(\"FormLabel\", {\n            id: `'${fieldId}'`,\n            fieldName: `'${fieldName}'`,\n            record: `__comp__.props.record`,\n            fieldInfo: `__comp__.props.archInfo.fieldNodes['${fieldId}']`,\n            className: `\"${label.className}\"`,\n            string: labelText,\n        });\n        const condition = label.getAttribute(\"t-if\");\n        if (condition) {\n            formLabel.setAttribute(\"t-if\", condition);\n        }\n        return formLabel;\n    }\n\n    /**\n     * @param {string} fieldName\n     * @returns {Element[]}\n     */\n    getLabels(fieldName) {\n        const labels = this.labels[fieldName] || [];\n        this.labels[fieldName] = null;\n        return labels;\n    }\n\n    /**\n     * @param {string} fieldName\n     * @param {Element} label\n     */\n    pushLabel(fieldName, label) {\n        this.labels[fieldName] = this.labels[fieldName] || [];\n        this.labels[fieldName].push(label);\n    }\n\n    //-----------------------------------------------------------------------------\n    // Compilers\n    //-----------------------------------------------------------------------------\n\n    /**\n     * @param {Element} el\n     * @param {Record<string, any>} params\n     * @returns {Element}\n     */\n    compileButtonBox(el, params) {\n        if (!el.children.length) {\n            return this.compileGenericNode(el, params);\n        }\n\n        el.classList.remove(\"oe_button_box\");\n        const buttonBox = createElement(\"ButtonBox\");\n        buttonBox.setAttribute(\"t-if\", \"!__comp__.env.inDialog\");\n        let slotId = 0;\n        let hasContent = false;\n        for (const child of el.children) {\n            const invisible = getModifier(child, \"invisible\");\n            if (!params.compileInvisibleNodes && (invisible === \"True\" || invisible === \"1\")) {\n                continue;\n            }\n            hasContent = true;\n            let isVisibleExpr;\n            if (!invisible || invisible === \"False\" || invisible === \"0\") {\n                isVisibleExpr = \"true\";\n            } else if (invisible === \"True\" || invisible === \"1\") {\n                isVisibleExpr = \"false\";\n            } else {\n                isVisibleExpr = `!__comp__.evaluateBooleanExpr(${JSON.stringify(\n                    invisible\n                )},__comp__.props.record.evalContextWithVirtualIds)`;\n            }\n            const mainSlot = createElement(\"t\", {\n                \"t-set-slot\": `slot_${slotId++}`,\n                isVisible: isVisibleExpr,\n            });\n            if (child.tagName === \"button\" || child.children.tagName === \"button\") {\n                child.classList.add(\n                    \"oe_stat_button\",\n                    \"btn\",\n                    \"btn-outline-secondary\",\n                    \"flex-grow-1\",\n                    \"flex-lg-grow-0\"\n                );\n            }\n            if (child.tagName === \"field\") {\n                child.classList.add(\"d-inline-block\", \"mb-0\", \"z-0\");\n            }\n            append(mainSlot, this.compileNode(child, params, false));\n            append(buttonBox, mainSlot);\n        }\n\n        return hasContent ? buttonBox : \"\";\n    }\n\n    compileButton(el, params) {\n        return super.compileButton(el, params);\n    }\n\n    /**\n     * @override\n     */\n    compileField(el, params) {\n        const field = super.compileField(el, params);\n\n        const fieldName = el.getAttribute(\"name\");\n        const fieldString = el.getAttribute(\"string\");\n        const fieldId = el.getAttribute(\"field_id\");\n        const labelsForAttr = el.getAttribute(\"id\") || fieldName;\n        const labels = this.getLabels(labelsForAttr);\n        const dynamicLabel = (label) => {\n            const formLabel = this.createLabelFromField(fieldId, fieldName, fieldString, label, {\n                ...params,\n                currentFieldArchNode: el,\n            });\n            if (formLabel) {\n                label.replaceWith(formLabel);\n            } else {\n                label.remove();\n            }\n            return formLabel;\n        };\n        for (const label of labels) {\n            dynamicLabel(label);\n        }\n        this.encounteredFields[fieldName] = dynamicLabel;\n        return field;\n    }\n\n    /**\n     * @param {Element} el\n     * @param {Record<string, any>} params\n     * @returns {Element}\n     */\n    compileForm(el, params) {\n        let sheetNode = null;\n        for (const sheet of el.querySelectorAll(\"sheet\")) {\n            if (sheet.closest(\"form\") === el) {\n                sheetNode = sheet;\n                break;\n            }\n        }\n        const displayClasses = sheetNode\n            ? `d-flex d-print-block {{ __comp__.uiService.size < ${SIZES.XXL} ? \"flex-column\" : \"flex-nowrap h-100\" }}`\n            : \"d-block\";\n        const stateClasses =\n            \"{{ __comp__.props.record.dirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}\";\n        const form = createElement(\"div\", {\n            class: \"o_form_renderer\",\n            \"t-att-class\": \"__comp__.props.class\",\n            \"t-attf-class\": `{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} ${displayClasses} ${stateClasses}`,\n        });\n        if (!sheetNode) {\n            for (const child of el.childNodes) {\n                // ButtonBox are already compiled for the control panel and should not\n                // be recompiled for the renderer of the view\n                if (child.attributes?.name?.value !== \"button_box\") {\n                    append(form, this.compileNode(child, params));\n                }\n            }\n            form.classList.add(\"o_form_nosheet\");\n        } else {\n            let compiledList = [];\n            for (const child of el.childNodes) {\n                const compiled = this.compileNode(child, params);\n                if (getTag(child, true) === \"sheet\") {\n                    append(form, compiled);\n                    compiled.prepend(...compiledList);\n                    compiledList = [];\n                } else if (compiled) {\n                    compiledList.push(compiled);\n                }\n            }\n            append(form, compiledList);\n        }\n        return form;\n    }\n\n    /**\n     * @param {Element} el\n     * @param {Record<string, any>} params\n     * @returns {Element}\n     */\n    compileFooter(el, params) {\n        const footer = createElement(\"t\");\n        const replace = el.getAttribute(\"replace\");\n        if (replace && !exprToBoolean(replace)) {\n            footer.append(\n                createElement(\"t\", {\n                    \"t-call\": \"web.DefaultButtonsSlot\",\n                    \"t-call-context\": \"{ props: __comp__.props }\",\n                })\n            );\n        }\n        copyAttributes(el, footer);\n        for (const child of el.childNodes) {\n            const compiled = this.compileNode(child, params);\n            if (compiled) {\n                footer.append(compiled);\n            }\n        }\n        return footer;\n    }\n\n    /**\n     * @param {Element} el\n     * @param {Record<string, any>} params\n     * @returns {Element}\n     */\n    compileGroup(el, params) {\n        const isOuterGroup = [...el.children].some((c) => getTag(c, true) === \"group\");\n        const formGroup = createElement(isOuterGroup ? \"OuterGroup\" : \"InnerGroup\");\n\n        let slotId = 0;\n        let sequence = 0;\n\n        if (el.hasAttribute(\"col\")) {\n            formGroup.setAttribute(\"maxCols\", el.getAttribute(\"col\"));\n        }\n\n        if (el.hasAttribute(\"string\")) {\n            const titleSlot = createElement(\"t\", { \"t-set-slot\": \"title\" }, [\n                makeSeparator(el.getAttribute(\"string\")),\n            ]);\n            append(formGroup, titleSlot);\n        }\n\n        let forceNewline = false;\n        for (const child of el.children) {\n            if (getTag(child, true) === \"newline\") {\n                forceNewline = true;\n                continue;\n            }\n\n            const invisible = getModifier(child, \"invisible\");\n            if (!params.compileInvisibleNodes && (invisible === \"True\" || invisible === \"1\")) {\n                continue;\n            }\n\n            const mainSlot = createElement(\"t\", {\n                \"t-set-slot\": `item_${slotId++}`,\n                type: \"'item'\",\n                sequence: sequence++,\n                \"t-slot-scope\": \"scope\",\n            });\n            let itemSpan = parseInt(child.getAttribute(\"colspan\") || \"1\", 10);\n\n            if (forceNewline) {\n                mainSlot.setAttribute(\"newline\", true);\n                forceNewline = false;\n            }\n\n            if (getTag(child, true) === \"separator\") {\n                itemSpan = parseInt(formGroup.getAttribute(\"maxCols\") || 2, 10);\n            }\n\n            if (child.matches(\"div[class='clearfix']:empty\")) {\n                itemSpan = parseInt(formGroup.getAttribute(\"maxCols\") || 2, 10);\n            }\n\n            let slotContent;\n            if (getTag(child, true) === \"field\") {\n                const addLabel = child.hasAttribute(\"nolabel\")\n                    ? child.getAttribute(\"nolabel\") !== \"1\"\n                    : true;\n                slotContent = this.compileNode(child, { ...params, currentSlot: mainSlot }, false);\n                if (slotContent && addLabel && !isOuterGroup && !isTextNode(slotContent)) {\n                    itemSpan = itemSpan === 1 ? itemSpan + 1 : itemSpan;\n                    const fieldName = child.getAttribute(\"name\");\n                    const fieldId = slotContent.getAttribute(\"id\") || fieldName;\n                    const props = {\n                        id: `${fieldId}`,\n                        fieldName: `'${fieldName}'`,\n                        record: `__comp__.props.record`,\n                        string: child.hasAttribute(\"string\")\n                            ? toStringExpression(child.getAttribute(\"string\"))\n                            : `__comp__.props.record.fields.${fieldName}.string`,\n                        fieldInfo: `__comp__.props.archInfo.fieldNodes[${fieldId}]`,\n                    };\n                    mainSlot.setAttribute(\"props\", objectToString(props));\n                    mainSlot.setAttribute(\"Component\", \"__comp__.constructor.components.FormLabel\");\n                    mainSlot.setAttribute(\"subType\", \"'item_component'\");\n                }\n            } else {\n                // TODO: When every apps will be revamp, we could remove the condition using 'o_td_label' in favor of 'o_wrap_label'\n                if (\n                    child.classList.contains(\"o_wrap_label\") ||\n                    child.classList.contains(\"o_td_label\") ||\n                    getTag(child, true) === \"label\"\n                ) {\n                    mainSlot.setAttribute(\"subType\", \"'label'\");\n                    child.classList.remove(\"o_wrap_label\");\n                }\n                slotContent = this.compileNode(child, { ...params, currentSlot: mainSlot }, false);\n            }\n\n            if (slotContent && !isTextNode(slotContent)) {\n                let isVisibleExpr;\n                if (!invisible || invisible === \"False\" || invisible === \"0\") {\n                    isVisibleExpr = \"true\";\n                } else if (invisible === \"True\" || invisible === \"1\") {\n                    isVisibleExpr = \"false\";\n                } else {\n                    isVisibleExpr = `!__comp__.evaluateBooleanExpr(${JSON.stringify(\n                        invisible\n                    )},__comp__.props.record.evalContextWithVirtualIds)`;\n                }\n                mainSlot.setAttribute(\"isVisible\", isVisibleExpr);\n                if (itemSpan > 0) {\n                    mainSlot.setAttribute(\"itemSpan\", `${itemSpan}`);\n                }\n\n                const groupClassExpr = `scope && scope.className`;\n                if (isComponentNode(slotContent)) {\n                    if (getTag(slotContent) === \"FormLabel\") {\n                        mainSlot.prepend(\n                            createElement(\"t\", {\n                                \"t-set\": \"addClass\",\n                                \"t-value\": groupClassExpr,\n                            })\n                        );\n                        combineAttributes(\n                            slotContent,\n                            \"className\",\n                            `(addClass ? \" \" + addClass : \"\")`,\n                            `+`\n                        );\n                    } else if (getTag(child, true) !== \"button\") {\n                        if (slotContent.hasAttribute(\"class\")) {\n                            mainSlot.prepend(\n                                createElement(\"t\", {\n                                    \"t-set\": \"addClass\",\n                                    \"t-value\": groupClassExpr,\n                                })\n                            );\n                            combineAttributes(\n                                slotContent,\n                                \"class\",\n                                `(addClass ? \" \" + addClass : \"\")`,\n                                `+`\n                            );\n                        } else {\n                            slotContent.setAttribute(\"class\", groupClassExpr);\n                        }\n                    }\n                } else {\n                    appendAttf(slotContent, \"class\", `${groupClassExpr} || \"\"`);\n                }\n                append(mainSlot, slotContent);\n                append(formGroup, mainSlot);\n            }\n        }\n        return formGroup;\n    }\n\n    /**\n     * @param {Element} el\n     * @param {Record<string, any>} params\n     * @returns {Element}\n     */\n    compileHeader(el, params) {\n        const statusBar = createElement(\"div\");\n        statusBar.className =\n            \"o_form_statusbar position-relative d-flex justify-content-between mb-0 mb-md-2 pb-2 pb-md-0\";\n        const buttons = [];\n        const others = [];\n        for (const child of el.childNodes) {\n            const compiled = this.compileNode(child, params);\n            if (!compiled || isTextNode(compiled)) {\n                continue;\n            }\n            if (getTag(child, true) === \"field\" && !child.classList.contains(\"btn\")) {\n                compiled.setAttribute(\"showTooltip\", true);\n                others.push(compiled);\n            } else {\n                if (compiled.tagName === \"ViewButton\") {\n                    compiled.setAttribute(\"defaultRank\", \"'btn-secondary'\");\n                }\n                buttons.push(compiled);\n            }\n        }\n        let slotId = 0;\n        let statusBarButtons;\n        if (params.asDropdownItems) {\n            statusBarButtons = createElement(\"StatusBarDropdownItems\");\n        } else {\n            statusBarButtons = createElement(\"StatusBarButtons\", {\n                \"t-if\": \"!__comp__.env.isSmall or __comp__.env.inDialog\",\n            });\n        }\n        for (const button of buttons) {\n            const slot = createElement(\"t\", {\n                \"t-set-slot\": `button_${slotId++}`,\n                isVisible: button.getAttribute(\"t-if\") || true,\n            });\n            append(slot, button);\n            append(statusBarButtons, slot);\n        }\n        if (params.asDropdownItems) {\n            return statusBarButtons;\n        }\n        append(statusBar, statusBarButtons);\n        append(statusBar, others);\n        return statusBar;\n    }\n\n    /**\n     * @param {Element} el\n     * @param {Record<string, any>} params\n     * @returns {Element}\n     */\n    compileLabel(el, params) {\n        const forAttr = el.getAttribute(\"for\");\n        // A label can contain or not the labelable Element it is referring to.\n        // If it doesn't, there is no `for=`\n        // Otherwise, the targetted element is somewhere else among its nextChildren\n        if (forAttr) {\n            let label = createElement(\"label\");\n            copyAttributes(el, label);\n            const string = el.getAttribute(\"string\");\n            if (string) {\n                append(label, createTextNode(string));\n            } else if (string === \"\") {\n                label.setAttribute(\"data-no-label\", \"true\");\n            }\n            if (this.encounteredFields[forAttr]) {\n                label = this.encounteredFields[forAttr](label);\n            } else {\n                this.pushLabel(forAttr, label);\n            }\n            return label;\n        }\n        const res = this.compileGenericNode(el, params);\n        copyAttributes(el, res);\n        return res;\n    }\n\n    /**\n     * @param {Element} el\n     * @param {Record<string, any>} params\n     * @returns {Element}\n     */\n    compileNotebook(el, params) {\n        const noteBookId = this.noteBookId++;\n        const noteBook = createElement(\"Notebook\");\n        const pageAnchors = [];\n        const noteBookAnchors = {};\n\n        if (el.hasAttribute(\"class\")) {\n            noteBook.setAttribute(\"className\", toStringExpression(el.getAttribute(\"class\")));\n            el.removeAttribute(\"class\");\n        }\n\n        noteBook.setAttribute(\n            \"defaultPage\",\n            `__comp__.props.record.isNew ? undefined : __comp__.props.activeNotebookPages[${noteBookId}]`\n        );\n        noteBook.setAttribute(\n            \"onPageUpdate\",\n            `(page) => __comp__.props.onNotebookPageChange(${noteBookId}, page)`\n        );\n\n        for (const child of el.children) {\n            if (getTag(child, true) !== \"page\") {\n                continue;\n            }\n            const invisible = getModifier(child, \"invisible\");\n            if (!params.compileInvisibleNodes && (invisible === \"True\" || invisible === \"1\")) {\n                continue;\n            }\n\n            const pageSlot = createElement(\"t\");\n            append(noteBook, pageSlot);\n\n            const pageId = `page_${this.id++}`;\n            const pageTitle = toStringExpression(\n                child.getAttribute(\"string\") || child.getAttribute(\"name\") || \"\"\n            );\n            const pageNodeName = toStringExpression(child.getAttribute(\"name\") || \"\");\n\n            pageSlot.setAttribute(\"t-set-slot\", pageId);\n            pageSlot.setAttribute(\"title\", pageTitle);\n            pageSlot.setAttribute(\"name\", pageNodeName);\n            if (child.className) {\n                pageSlot.setAttribute(\"className\", `\"${child.className}\"`);\n            }\n\n            if (child.getAttribute(\"autofocus\") === \"autofocus\") {\n                noteBook.setAttribute(\n                    \"defaultPage\",\n                    `__comp__.props.record.isNew ? \"${pageId}\" : (__comp__.props.activeNotebookPages[${noteBookId}] || \"${pageId}\")`\n                );\n            }\n\n            for (const anchor of child.querySelectorAll(\"[href^=\\\\#]\")) {\n                const anchorValue = CSS.escape(anchor.getAttribute(\"href\").substring(1));\n                if (!anchorValue.length) {\n                    continue;\n                }\n                pageAnchors.push(anchorValue);\n                noteBookAnchors[anchorValue] = {\n                    origin: `'${pageId}'`,\n                };\n            }\n\n            let isVisibleExpr;\n            if (!invisible || invisible === \"False\" || invisible === \"0\") {\n                isVisibleExpr = \"true\";\n            } else if (invisible === \"True\" || invisible === \"1\") {\n                isVisibleExpr = \"false\";\n            } else {\n                isVisibleExpr = `!__comp__.evaluateBooleanExpr(${JSON.stringify(\n                    invisible\n                )},__comp__.props.record.evalContextWithVirtualIds)`;\n            }\n            pageSlot.setAttribute(\"isVisible\", isVisibleExpr);\n\n            for (const contents of child.children) {\n                append(pageSlot, this.compileNode(contents, { ...params, currentSlot: pageSlot }));\n            }\n        }\n\n        if (pageAnchors.length) {\n            // If anchors from the page are targetting an element\n            // present in the notebook, it must be aware of the\n            // page that contains the corresponding element\n            for (const anchor of pageAnchors) {\n                let pageId = 1;\n                for (const child of el.children) {\n                    if (child.querySelector(`#${anchor}`)) {\n                        noteBookAnchors[anchor].target = `'page_${pageId}'`;\n                        noteBookAnchors[anchor] = objectToString(noteBookAnchors[anchor]);\n                        break;\n                    }\n                    pageId++;\n                }\n            }\n            noteBook.setAttribute(\"anchors\", objectToString(noteBookAnchors));\n        }\n\n        return noteBook;\n    }\n\n    /**\n     * @param {Element} el\n     * @param {Record<string, any>} params\n     * @returns {Element}\n     */\n    compileSetting(el, params) {\n        const setting = createElement(params.componentName || \"Setting\", {\n            title: toStringExpression(el.getAttribute(\"title\") || \"\"),\n            help: toStringExpression(el.getAttribute(\"help\") || \"\"),\n            companyDependent: el.getAttribute(\"company_dependent\") === \"1\" || \"false\",\n            documentation: toStringExpression(el.getAttribute(\"documentation\") || \"\"),\n            record: `__comp__.props.record`,\n        });\n        if (el.getAttribute(\"id\")) {\n            setting.setAttribute(\"id\", toStringExpression(el.getAttribute(\"id\")));\n        }\n        let string = toStringExpression(el.getAttribute(\"string\") || \"\");\n        let addLabel = true;\n        Array.from(el.children).forEach((child, index) => {\n            if (getTag(child, true) === \"field\" && index === 0) {\n                const fieldSlot = createElement(\"t\", { \"t-set-slot\": \"fieldSlot\" });\n                const field = this.compileNode(child, params);\n                if (field) {\n                    append(fieldSlot, field);\n                    setting.setAttribute(\"fieldInfo\", field.getAttribute(\"fieldInfo\"));\n\n                    addLabel = child.hasAttribute(\"nolabel\")\n                        ? child.getAttribute(\"nolabel\") !== \"1\"\n                        : true;\n                    const fieldName = child.getAttribute(\"name\");\n                    string = child.hasAttribute(\"string\")\n                        ? toStringExpression(child.getAttribute(\"string\"))\n                        : string;\n                    setting.setAttribute(\"fieldName\", toStringExpression(fieldName));\n                    setting.setAttribute(\n                        \"fieldId\",\n                        toStringExpression(child.getAttribute(\"field_id\"))\n                    );\n                }\n                append(setting, fieldSlot);\n            } else {\n                append(setting, this.compileNode(child, params));\n            }\n        });\n        setting.setAttribute(\"string\", string);\n        setting.setAttribute(\"addLabel\", addLabel);\n        return setting;\n    }\n\n    /**\n     * @param {Element} el\n     * @param {Record<string, any>} params\n     * @returns {Element}\n     */\n    compileSeparator(el, params = {}) {\n        const separator = makeSeparator(el.getAttribute(\"string\"));\n        copyAttributes(el, separator);\n        return this.applyInvisible(getModifier(el, \"invisible\"), separator, params);\n    }\n\n    /**\n     * @param {Element} el\n     * @param {Record<string, any>} params\n     * @returns {Element}\n     */\n    compileSheet(el, params) {\n        const sheetBG = createElement(\"div\");\n        sheetBG.className = \"o_form_sheet_bg\";\n\n        const sheetFG = createElement(\"div\");\n        sheetFG.className = \"o_form_sheet position-relative\";\n\n        append(sheetBG, sheetFG);\n        for (const child of el.childNodes) {\n            const compiled = this.compileNode(child, params);\n            if (!compiled) {\n                continue;\n            }\n            if (compiled.nodeName === \"ButtonBox\") {\n                // in form views with a sheet, the button box is moved to the\n                // control panel, and in dialogs, there's no button box\n                continue;\n            }\n            if (getTag(child, true) === \"field\") {\n                compiled.setAttribute(\"showTooltip\", true);\n            }\n            append(sheetFG, compiled);\n        }\n        return sheetBG;\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { hasTouch } from \"@web/core/browser/feature_detection\";\nimport {\n    deleteConfirmationMessage,\n    ConfirmationDialog,\n} from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { makeContext } from \"@web/core/context\";\nimport { useDebugCategory } from \"@web/core/debug/debug_context\";\nimport { registry } from \"@web/core/registry\";\nimport { SIZES } from \"@web/core/ui/ui_service\";\nimport { user } from \"@web/core/user\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { omit } from \"@web/core/utils/objects\";\nimport { createElement, parseXML } from \"@web/core/utils/xml\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { useSetupAction } from \"@web/search/action_hook\";\nimport { Layout } from \"@web/search/layout\";\nimport { usePager } from \"@web/search/pager_hook\";\nimport { standardViewProps } from \"@web/views/standard_view_props\";\nimport { isX2Many } from \"@web/views/utils\";\nimport { executeButtonCallback, useViewButtons } from \"@web/views/view_button/view_button_hook\";\nimport { ViewButton } from \"@web/views/view_button/view_button\";\nimport { Field } from \"@web/views/fields/field\";\nimport { useModel } from \"@web/model/model\";\nimport { addFieldDependencies, extractFieldsFromArchInfo } from \"@web/model/relational_model/utils\";\nimport { useViewCompiler } from \"@web/views/view_compiler\";\nimport { Widget } from \"@web/views/widgets/widget\";\nimport { STATIC_ACTIONS_GROUP_NUMBER } from \"@web/search/action_menus/action_menus\";\n\nimport { ButtonBox } from \"./button_box/button_box\";\nimport { FormCompiler } from \"./form_compiler\";\nimport { FormErrorDialog } from \"./form_error_dialog/form_error_dialog\";\nimport { FormStatusIndicator } from \"./form_status_indicator/form_status_indicator\";\nimport { StatusBarDropdownItems } from \"./status_bar_dropdown_items/status_bar_dropdown_items\";\nimport { FormCogMenu } from \"./form_cog_menu/form_cog_menu\";\n\nimport {\n    Component,\n    onError,\n    onMounted,\n    onRendered,\n    onWillUnmount,\n    status,\n    useComponent,\n    useEffect,\n    useRef,\n    useState,\n} from \"@odoo/owl\";\nimport { FetchRecordError } from \"@web/model/relational_model/errors\";\nimport { effect } from \"@web/core/utils/reactive\";\n\nconst viewRegistry = registry.category(\"views\");\n\nexport async function loadSubViews(fieldNodes, fields, context, resModel, viewService, isSmall) {\n    for (const fieldInfo of Object.values(fieldNodes)) {\n        const fieldName = fieldInfo.name;\n        const field = fields[fieldName];\n        if (!isX2Many(field)) {\n            continue; // what follows only concerns x2many fields\n        }\n        if (fieldInfo.invisible === \"True\" || fieldInfo.invisible === \"1\") {\n            continue; // no need to fetch the sub view if the field is always invisible\n        }\n        if (!fieldInfo.field.useSubView) {\n            continue; // the FieldComponent used to render the field doesn't need a sub view\n        }\n\n        fieldInfo.views = fieldInfo.views || {};\n        let viewType = fieldInfo.viewMode || \"list,kanban\";\n        if (viewType.includes(\",\")) {\n            viewType = isSmall ? \"kanban\" : \"list\";\n        }\n        fieldInfo.viewMode = viewType;\n        if (fieldInfo.views[viewType]) {\n            continue; // the sub view is inline in the main form view\n        }\n\n        // extract *_view_ref keys from field context, to fetch the adequate view\n        const fieldContext = {};\n        const regex = /'([a-z]*_view_ref)' *: *'(.*?)'/g;\n        let matches;\n        while ((matches = regex.exec(fieldInfo.context)) !== null) {\n            fieldContext[matches[1]] = matches[2];\n        }\n        // filter out *_view_ref keys from general context\n        const refinedContext = {};\n        for (const key in context) {\n            if (key.indexOf(\"_view_ref\") === -1) {\n                refinedContext[key] = context[key];\n            }\n        }\n\n        const comodel = field.relation;\n        const {\n            fields: comodelFields,\n            relatedModels,\n            views,\n        } = await viewService.loadViews({\n            resModel: comodel,\n            views: [[false, viewType]],\n            context: makeContext([fieldContext, user.context, refinedContext]),\n        });\n        const { ArchParser } = viewRegistry.get(viewType);\n        const xmlDoc = parseXML(views[viewType].arch);\n        const archInfo = new ArchParser().parse(xmlDoc, relatedModels, comodel);\n        fieldInfo.views[viewType] = {\n            ...archInfo,\n            limit: archInfo.limit || 40,\n            fields: comodelFields,\n        };\n        fieldInfo.relatedFields = comodelFields;\n    }\n}\n\nexport function useFormViewInDialog() {\n    const component = useComponent();\n    onMounted(() => {\n        component.env.bus.trigger(\"FORM-CONTROLLER:FORM-IN-DIALOG:ADD\");\n    });\n\n    onWillUnmount(() => {\n        component.env.bus.trigger(\"FORM-CONTROLLER:FORM-IN-DIALOG:REMOVE\");\n    });\n}\n// -----------------------------------------------------------------------------\n\nexport class FormController extends Component {\n    static template = `web.FormView`;\n    static components = {\n        FormStatusIndicator,\n        Layout,\n        ButtonBox,\n        ViewButton,\n        Field,\n        CogMenu: FormCogMenu,\n        StatusBarDropdownItems,\n        Widget,\n    };\n\n    static props = {\n        ...standardViewProps,\n        discardRecord: { type: Function, optional: true },\n        mode: {\n            optional: true,\n            validate: (m) => [\"edit\", \"readonly\"].includes(m),\n        },\n        saveRecord: { type: Function, optional: true },\n        removeRecord: { type: Function, optional: true },\n        Model: Function,\n        Renderer: Function,\n        Compiler: Function,\n        archInfo: Object,\n        buttonTemplate: String,\n        preventCreate: { type: Boolean, optional: true },\n        preventEdit: { type: Boolean, optional: true },\n        onDiscard: { type: Function, optional: true },\n        onSave: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        preventCreate: false,\n        preventEdit: false,\n        updateActionState: () => {},\n    };\n\n    setup() {\n        this.evaluateBooleanExpr = evaluateBooleanExpr;\n        this.dialogService = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n        this.viewService = useService(\"view\");\n        this.ui = useService(\"ui\");\n        this.companyService = useService(\"company\");\n        useBus(this.ui.bus, \"resize\", this.render);\n\n        this.archInfo = this.props.archInfo;\n        const { create, edit } = this.archInfo.activeActions;\n        this.canCreate = create && !this.props.preventCreate;\n        this.canEdit = edit && !this.props.preventEdit;\n        this.duplicateId = false;\n\n        this.display = { ...this.props.display };\n        if (this.env.inDialog) {\n            this.display.controlPanel = false;\n        }\n\n        this.formInDialog = 0;\n\n        useBus(this.env.bus, \"FORM-CONTROLLER:FORM-IN-DIALOG:ADD\", () => this.formInDialog++);\n        useBus(this.env.bus, \"FORM-CONTROLLER:FORM-IN-DIALOG:REMOVE\", () => this.formInDialog--);\n\n        const beforeFirstLoad = async () => {\n            await loadSubViews(\n                this.archInfo.fieldNodes,\n                this.props.fields,\n                this.props.context,\n                this.props.resModel,\n                this.viewService,\n                this.env.isSmall\n            );\n            const { activeFields, fields } = extractFieldsFromArchInfo(\n                this.archInfo,\n                this.props.fields\n            );\n            if (this.display.controlPanel) {\n                addFieldDependencies(activeFields, fields, [\n                    { name: \"display_name\", type: \"char\", readonly: true },\n                ]);\n            }\n            this.model.config.activeFields = activeFields;\n            this.model.config.fields = fields;\n        };\n        this.model = useState(useModel(this.props.Model, this.modelParams, { beforeFirstLoad }));\n\n        onMounted(() => {\n            effect(\n                (model) => {\n                    if (status(this) === \"mounted\") {\n                        this.props.updateActionState({ resId: model.root.resId });\n                    }\n                },\n                [this.model]\n            );\n        });\n\n        onError((error) => {\n            const suggestedCompany = error.cause?.data?.context?.suggested_company;\n            if (\n                error.cause?.data?.name === \"odoo.exceptions.AccessError\" &&\n                suggestedCompany &&\n                !this.env.inDialog\n            ) {\n                this.env.pushStateBeforeReload();\n                const activeCompanyIds = this.companyService.activeCompanyIds;\n                activeCompanyIds.push(suggestedCompany.id);\n                this.companyService.setCompanies(activeCompanyIds, true);\n            } else {\n                throw error;\n            }\n        });\n\n        // select footers that are not in subviews and move them to another arch\n        // that will be moved to the dialog's footer (if we are in a dialog)\n        const footers = [...this.archInfo.xmlDoc.querySelectorAll(\"footer:not(field footer)\")];\n        if (footers.length) {\n            this.footerArchInfo = Object.assign({}, this.archInfo);\n            this.footerArchInfo.xmlDoc = createElement(\"t\");\n            this.footerArchInfo.xmlDoc.append(...footers);\n            this.footerArchInfo.arch = this.footerArchInfo.xmlDoc.outerHTML;\n            this.archInfo.arch = this.archInfo.xmlDoc.outerHTML;\n        }\n\n        const xmlDocButtonBox = this.archInfo.xmlDoc.querySelector(\n            \"div[name='button_box']:not(field div)\"\n        );\n        if (xmlDocButtonBox) {\n            const buttonBoxTemplates = useViewCompiler(\n                this.props.Compiler || FormCompiler,\n                { ButtonBox: xmlDocButtonBox },\n                { isSubView: true }\n            );\n            this.buttonBoxTemplate = buttonBoxTemplates.ButtonBox;\n        }\n\n        const xmlDocHeader = this.archInfo.xmlDoc.querySelector(\"header\");\n        if (xmlDocHeader) {\n            const { StatusBarDropdownItems } = useViewCompiler(\n                this.props.Compiler || FormCompiler,\n                { StatusBarDropdownItems: xmlDocHeader },\n                { isSubView: true, asDropdownItems: true }\n            );\n            this.statusBarDropdownItemsTemplate = StatusBarDropdownItems;\n        }\n\n        this.rootRef = useRef(\"root\");\n        useViewButtons(this.rootRef, {\n            beforeExecuteAction: this.beforeExecuteActionButton.bind(this),\n            afterExecuteAction: this.afterExecuteActionButton.bind(this),\n            reload: () => this.model.load(),\n        });\n\n        const state = this.props.state || {};\n        const activeNotebookPages = { ...state.activeNotebookPages };\n        this.onNotebookPageChange = (notebookId, page) => {\n            if (page) {\n                activeNotebookPages[notebookId] = page;\n            }\n        };\n\n        useSetupAction({\n            rootRef: this.rootRef,\n            beforeVisibilityChange: () => this.beforeVisibilityChange(),\n            beforeLeave: () => this.beforeLeave(),\n            beforeUnload: (ev) => this.beforeUnload(ev),\n            getLocalState: () => {\n                return {\n                    activeNotebookPages: !this.model.root.isNew ? activeNotebookPages : {},\n                    modelState: this.model.exportState(),\n                    resId: this.model.root.resId,\n                };\n            },\n        });\n        useDebugCategory(\"form\", { component: this });\n\n        usePager(() => {\n            if (!this.model.root.isNew) {\n                const resIds = this.model.root.resIds;\n                return {\n                    offset: resIds.indexOf(this.model.root.resId),\n                    limit: 1,\n                    total: resIds.length,\n                    onUpdate: ({ offset }) => this.onPagerUpdate({ offset, resIds }),\n                };\n            }\n        });\n\n        onRendered(() => {\n            this.env.config.setDisplayName(this.displayName());\n        });\n\n        const { disableAutofocus } = this.archInfo;\n        if (!disableAutofocus) {\n            useEffect(\n                (isInEdition) => {\n                    if (\n                        !isInEdition &&\n                        !this.rootRef.el\n                            .querySelector(\".o_content\")\n                            .contains(document.activeElement)\n                    ) {\n                        const elementToFocus = this.rootRef.el.querySelector(\n                            \".o_content button.btn-primary\"\n                        );\n                        if (elementToFocus) {\n                            elementToFocus.focus();\n                        }\n                    }\n                },\n                () => [this.model.root.isInEdition]\n            );\n        }\n\n        if (this.env.inDialog) {\n            useFormViewInDialog();\n        }\n    }\n\n    get cogMenuProps() {\n        return {\n            getActiveIds: () => (this.model.root.isNew ? [] : [this.model.root.resId]),\n            context: this.props.context,\n            items: this.props.info.actionMenus ? this.actionMenuItems : {},\n            isDomainSelected: this.model.root.isDomainSelected,\n            resModel: this.model.root.resModel,\n            domain: this.props.domain,\n            onActionExecuted: () =>\n                this.model.load({ resId: this.model.root.resId, resIds: this.model.root.resIds }),\n            shouldExecuteAction: this.shouldExecuteAction.bind(this),\n        };\n    }\n\n    get modelParams() {\n        let mode = this.props.mode || \"edit\";\n        if (!this.canEdit && this.props.resId) {\n            mode = \"readonly\";\n        }\n        return {\n            config: {\n                resModel: this.props.resModel,\n                resId: this.props.resId || false,\n                resIds: this.props.resIds || (this.props.resId ? [this.props.resId] : []),\n                fields: this.props.fields,\n                activeFields: {}, // will be generated after loading sub views (see willStart)\n                isMonoRecord: true,\n                mode,\n                context: this.props.context,\n            },\n            state: this.props.state?.modelState,\n            hooks: {\n                onWillLoadRoot: this.onWillLoadRoot.bind(this),\n                onWillSaveRecord: this.onWillSaveRecord.bind(this),\n                onRecordSaved: this.onRecordSaved.bind(this),\n            },\n            useSendBeaconToSaveUrgently: true,\n        };\n    }\n\n    /**\n     * onWillLoadRoot is a callback that will be executed before (re)loading the\n     * data necessary for the root record datapoint. Note that this.model.root\n     * may not exist yet at this point, if this is the first load.\n     */\n    onWillLoadRoot() {\n        this.duplicateId = undefined;\n    }\n\n    /**\n     * onRecordSaved is a callBack that will be executed after the save\n     * if it was done. It will therefore not be executed if the record\n     * is invalid, if a server error is thrown, or if there are no\n     * changes to save.\n     * @param {Record} record\n     */\n    async onRecordSaved(record, changes) {\n        if (this.duplicateId === record.id) {\n            const translationChanges = {};\n            for (const fieldName in changes) {\n                if (record.fields[fieldName].translate) {\n                    translationChanges[fieldName] = changes[fieldName];\n                }\n            }\n            if (Object.keys(translationChanges).length) {\n                await this.orm.call(this.model.root.resModel, \"web_override_translations\", [\n                    [this.model.root.resId],\n                    translationChanges,\n                ]);\n            }\n        }\n    }\n\n    /**\n     * onWillSaveRecord is a callBack that will be executed before the\n     * record save if the record is valid if the record is valid.\n     * If it returns false, it will prevent the save.\n     * @param {Record} record\n     */\n    async onWillSaveRecord() {}\n\n    async onSaveError(error, { discard }) {\n        const proceed = await new Promise((resolve) => {\n            this.model.dialog.add(FormErrorDialog, {\n                message: error.data.message,\n                data: error.data,\n                onDiscard: () => {\n                    discard();\n                    resolve(true);\n                },\n                onStayHere: () => resolve(false),\n            });\n        });\n        return proceed;\n    }\n\n    displayName() {\n        return this.model.root.data.display_name || (this.model.root.isNew && _t(\"New\")) || \"\";\n    }\n\n    async onPagerUpdate({ offset, resIds }) {\n        const dirty = await this.model.root.isDirty();\n        try {\n            if (dirty) {\n                await this.model.root.save({\n                    onError: this.onSaveError.bind(this),\n                    nextId: resIds[offset],\n                });\n            } else {\n                await this.model.load({ resId: resIds[offset] });\n            }\n        } catch (e) {\n            if (e instanceof FetchRecordError) {\n                this.model.load({\n                    resIds: this.model.config.resIds.filter((id) => !e.resIds.includes(id)),\n                });\n            }\n            throw e;\n        }\n    }\n\n    beforeVisibilityChange() {\n        if (document.visibilityState === \"hidden\" && this.formInDialog === 0) {\n            return this.model.root.save();\n        }\n    }\n\n    async beforeLeave() {\n        if (this.model.root.dirty) {\n            return this.save({\n                reload: false,\n                onError: this.onSaveError.bind(this),\n            });\n        }\n    }\n\n    async beforeUnload(ev) {\n        const succeeded = await this.model.root.urgentSave();\n        if (!succeeded) {\n            ev.preventDefault();\n            ev.returnValue = \"Unsaved changes\";\n        }\n    }\n\n    getStaticActionMenuItems() {\n        const { activeActions } = this.archInfo;\n        return {\n            archive: {\n                isAvailable: () => this.archiveEnabled && this.model.root.isActive,\n                sequence: 10,\n                description: _t(\"Archive\"),\n                icon: \"oi oi-archive\",\n                callback: () => {\n                    this.dialogService.add(ConfirmationDialog, this.archiveDialogProps);\n                },\n            },\n            unarchive: {\n                isAvailable: () => this.archiveEnabled && !this.model.root.isActive,\n                sequence: 20,\n                icon: \"oi oi-unarchive\",\n                description: _t(\"Unarchive\"),\n                callback: () => this.model.root.unarchive(),\n            },\n            duplicate: {\n                isAvailable: () => activeActions.create && activeActions.duplicate,\n                sequence: 30,\n                icon: \"fa fa-clone\",\n                description: _t(\"Duplicate\"),\n                callback: () => this.duplicateRecord(),\n            },\n            delete: {\n                isAvailable: () => activeActions.delete && !this.model.root.isNew,\n                sequence: 40,\n                icon: \"fa fa-trash-o\",\n                description: _t(\"Delete\"),\n                callback: () => this.deleteRecord(),\n                skipSave: true,\n            },\n            addPropertyFieldValue: {\n                isAvailable: () => activeActions.addPropertyFieldValue,\n                sequence: 50,\n                icon: \"fa fa-cogs\",\n                description: _t(\"Add Properties\"),\n                callback: () => this.model.bus.trigger(\"PROPERTY_FIELD:ADD_PROPERTY_VALUE\"),\n            },\n        };\n    }\n\n    get archiveDialogProps() {\n        return {\n            body: _t(\"Are you sure that you want to archive this record?\"),\n            confirmLabel: _t(\"Archive\"),\n            confirm: () => this.model.root.archive(),\n            cancel: () => {},\n        };\n    }\n\n    get actionMenuItems() {\n        const { actionMenus } = this.props.info;\n        const staticActionItems = Object.entries(this.getStaticActionMenuItems())\n            .filter(([key, item]) => item.isAvailable === undefined || item.isAvailable())\n            .sort(([k1, item1], [k2, item2]) => (item1.sequence || 0) - (item2.sequence || 0))\n            .map(([key, item]) =>\n                Object.assign({ key }, omit(item, \"isAvailable\", \"sequence\"), {\n                    groupNumber: STATIC_ACTIONS_GROUP_NUMBER,\n                })\n            );\n\n        return {\n            action: [...staticActionItems, ...(actionMenus.action || [])],\n            print: actionMenus.print,\n        };\n    }\n\n    // enable the archive feature in Actions menu only if the active field is in the view\n    get archiveEnabled() {\n        return \"active\" in this.model.root.activeFields\n            ? !this.props.fields.active.readonly\n            : \"x_active\" in this.model.root.activeFields\n            ? !this.props.fields.x_active.readonly\n            : false;\n    }\n\n    async shouldExecuteAction(item) {\n        const dirty = await this.model.root.isDirty();\n        if ((dirty || this.model.root.isNew) && !item.skipSave) {\n            let hasError = false;\n            const isSaved = await this.model.root.save({\n                onError: (...args) => {\n                    hasError = true;\n                    return this.onSaveError(...args);\n                },\n            });\n            return isSaved && !hasError;\n        }\n        return true;\n    }\n\n    async duplicateRecord() {\n        await this.model.root.duplicate();\n        this.duplicateId = this.model.root.id;\n    }\n\n    get deleteConfirmationDialogProps() {\n        return {\n            title: _t(\"Bye-bye, record!\"),\n            body: deleteConfirmationMessage,\n            confirm: async () => {\n                await this.model.root.delete();\n                if (!this.model.root.resId) {\n                    this.env.config.historyBack();\n                }\n            },\n            confirmLabel: _t(\"Delete\"),\n            cancel: () => {},\n            cancelLabel: _t(\"No, keep it\"),\n        };\n    }\n\n    async deleteRecord() {\n        this.dialogService.add(ConfirmationDialog, this.deleteConfirmationDialogProps);\n    }\n\n    async beforeExecuteActionButton(clickParams) {\n        const record = this.model.root;\n        if (clickParams.special !== \"cancel\") {\n            let saved = false;\n            if (clickParams.special === \"save\" && this.props.saveRecord) {\n                saved = await this.props.saveRecord(record, clickParams);\n            } else {\n                const params = { reload: !(this.env.inDialog && clickParams.close) };\n                saved = await record.save(params);\n            }\n            if (saved !== false && this.props.onSave) {\n                this.props.onSave(record, clickParams);\n            }\n            return saved;\n        } else if (this.props.onDiscard) {\n            this.props.onDiscard(record);\n        }\n    }\n\n    async afterExecuteActionButton(clickParams) {}\n\n    async create() {\n        const dirty = await this.model.root.isDirty();\n        const onError = this.onSaveError.bind(this);\n        const canProceed = !dirty || (await this.model.root.save({ onError }));\n        // FIXME: disable/enable not done in onPagerUpdate\n        if (canProceed) {\n            await executeButtonCallback(this.ui.activeElement, () =>\n                this.model.load({ resId: false })\n            );\n        }\n    }\n\n    async save(params) {\n        const record = this.model.root;\n        let saved = false;\n        if (this.props.saveRecord) {\n            saved = await this.props.saveRecord(record, params);\n        } else {\n            saved = await record.save(params);\n        }\n        if (saved && this.props.onSave) {\n            this.props.onSave(record, params);\n        }\n        return saved;\n    }\n\n    saveButtonClicked(params = {}) {\n        return executeButtonCallback(this.ui.activeElement, () => this.save(params));\n    }\n\n    async discard() {\n        if (this.props.discardRecord) {\n            this.props.discardRecord(this.model.root);\n            return;\n        }\n        await this.model.root.discard();\n        if (this.props.onDiscard) {\n            this.props.onDiscard(this.model.root);\n        }\n        if (this.model.root.isNew || this.env.inDialog) {\n            this.env.config.historyBack();\n        }\n    }\n\n    get className() {\n        const result = {};\n        const { size } = this.ui;\n        if (size <= SIZES.XS) {\n            result.o_xxs_form_view = true;\n        } else if (!this.env.inDialog && size === SIZES.XXL) {\n            result[\"o_xxl_form_view h-100\"] = true;\n        }\n        if (this.props.className) {\n            result[this.props.className] = true;\n        }\n        result[\"o_field_highlight\"] = size < SIZES.SM || hasTouch();\n        return result;\n    }\n}\n", "/** @odoo-module */\n\nimport { FormController } from \"@web/views/form/form_controller\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { CallbackRecorder } from \"@web/search/action_hook\";\nimport {\n    useEffect,\n    useSubEnv\n} from \"@odoo/owl\";\n\n\n/**\n * Knowledge articles can interact with some records with the help of the\n * @see KnowledgeCommandsService if they have an html field.\n * The algorithm first searches for an html field name in the following list.\n * The list is ordered and the first match found in a record will take\n * precedence. If no match is found, any accessible html field in the view\n * will do, in order of declaration.\n * Once a match is found, it is stored in the\n * KnowledgeCommandsService to be accessed later by an article.\n * If the field is inside a Form notebook page, the page must have a name\n * attribute, or else it won't be considered as Knowledge macros won't be able\n * to access it through a selector.\n */\nconst KNOWLEDGE_RECORDED_FIELD_NAMES = [\n    'note',\n    'memo',\n    'description',\n    'comment',\n    'narration',\n    'additional_note',\n    'internal_notes',\n    'notes',\n];\n\n/**\n * Here are the models whose html fields won't be accessible through Knowledge.\n * A consideration to add a model in this list is how heavily modified their\n * form view is compared to the standard, because then the macro will not be\n * able to navigate them.\n */\nconst KNOWLEDGE_EXCLUDED_MODELS = new Set([\n    'knowledge.article',\n    'res.config.settings',\n]);\n\nconst FormControllerPatch = {\n    setup() {\n        super.setup(...arguments);\n        this.command = useService(\"command\");\n        if (!this.env.inDialog) {\n            this.knowledgeCommandsService = useService('knowledgeCommandsService');\n            useSubEnv({\n                __knowledgeUpdateCommandsRecordInfo__: new CallbackRecorder(),\n            });\n            useEffect(\n                () => this._evaluateRecordCandidate(),\n                () => [this.model.root.resId],\n            );\n        }\n    },\n    /**\n     * Evaluate the current record and register its relevant information in\n     * @see KnowledgeCommandsService if it can be used in a Knowledge article\n     * through a macro.\n     *\n     * The access rights information for the chatter is only available\n     * asynchronously after it is mounted, therefore populating this\n     * information for the `commandsRecordInfo` is delegated to the chatter\n     * through the __knowledgeUpdateCommandsRecordInfo__ callbackRecorder.\n     */\n    _evaluateRecordCandidate() {\n        if (\n            KNOWLEDGE_EXCLUDED_MODELS.has(this.props.resModel) ||\n            !this.env.config.breadcrumbs ||\n            !this.env.config.breadcrumbs.length\n        ) {\n            return;\n        }\n\n        const record = this.model.root;\n        const fields = this.props.fields;\n        const xmlDoc = this.props.archInfo.xmlDoc;\n        // format stored by the knowledgeCommandsService\n        const commandsRecordInfo = {\n            resId: this.model.root.resId,\n            resModel: this.props.resModel,\n            breadcrumbs: this.knowledgeCommandsService.getBreadcrumbsIdentifier(\n                this.env.config.breadcrumbs || []\n            ),\n            canPostMessages: false,\n            canAttachFiles: false,\n            withHtmlField: false,\n            fieldInfo: {},\n            xmlDoc: this.props.archInfo.xmlDoc,\n        };\n\n        // check whether the form view has a chatter\n        if (this.props.archInfo.xmlDoc.querySelector(\"chatter\")) {\n            for (const callback of this.env.__knowledgeUpdateCommandsRecordInfo__.callbacks) {\n                callback(commandsRecordInfo);\n            }\n        }\n\n        if (this.props.mode === \"readonly\" || !this.canEdit) {\n            return;\n        }\n\n        const defaultFieldNamesSet = new Set(KNOWLEDGE_RECORDED_FIELD_NAMES);\n        const fieldNames = KNOWLEDGE_RECORDED_FIELD_NAMES.filter((name) => {\n            return (\n                name in record.activeFields &&\n                fields[name].type === \"html\" &&\n                !fields[name].readonly\n            );\n        }).concat(\n            Object.getOwnPropertyNames(record.activeFields).filter((name) => {\n                return (\n                    !defaultFieldNamesSet.has(name) &&\n                    fields[name].type === \"html\" &&\n                    !fields[name].readonly\n                );\n            })\n        );\n\n        // check if there is any html field usable with knowledge\n        loopFieldNames: for (const fieldName of fieldNames) {\n            if (\n                evaluateBooleanExpr(record.activeFields[fieldName].readonly, record.evalContextWithVirtualIds) ||\n                evaluateBooleanExpr(record.activeFields[fieldName].invisible, record.evalContextWithVirtualIds)\n            ) {\n                continue loopFieldNames;\n            }\n            // Parse the xmlDoc to find all instances of the field that are\n            // not descendants of another field and whose parents are\n            // visible (relative to the current record's context). Evaluate\n            // the invisible and readonly modifiers attributes for each field\n            // instance in the xmlDoc. Don't consider html fields that are not\n            // using the `html` widget (i.e. mass_mailing_html widget).\n            const xmlFields = Array.from(xmlDoc.querySelectorAll(`field[name=\"${fieldName}\"]:is([widget=\"html\"], :not([widget])`));\n            const xmlFieldsCandidates = xmlFields.filter((xmlField) => {\n                return (\n                    !(xmlField.parentElement.closest('field')) &&\n                    !evaluateBooleanExpr(xmlField.getAttribute('readonly'), record.evalContextWithVirtualIds) &&\n                    !evaluateBooleanExpr(xmlField.getAttribute('invisible'), record.evalContextWithVirtualIds)\n                );\n            });\n            loopXmlFieldsCandidates: for (const xmlField of xmlFieldsCandidates) {\n                const xmlFieldParent = xmlField.parentElement;\n                let xmlInvisibleParent = xmlFieldParent.closest('[invisible]');\n                while (xmlInvisibleParent) {\n                    const invisibleParentModifier = xmlInvisibleParent.getAttribute('invisible');\n                    if (evaluateBooleanExpr(invisibleParentModifier, record.evalContextWithVirtualIds)) {\n                        continue loopXmlFieldsCandidates;\n                    }\n                    xmlInvisibleParent = xmlInvisibleParent.parentElement &&\n                        xmlInvisibleParent.parentElement.closest('[invisible]');\n                }\n                const page = xmlField.closest('page');\n                const pageName = page ? page.getAttribute('name') : undefined;\n                // If the field is inside an unnamed notebook page, ignore\n                // it as if it was unavailable, since the macro will not be\n                // able to open it to access the field (the name is used as\n                // a selector).\n                if (!page || pageName) {\n                    commandsRecordInfo.fieldInfo = {\n                        name: fieldName,\n                        string: fields[fieldName].string,\n                        pageName: pageName,\n                    };\n                    break loopFieldNames;\n                }\n            }\n        }\n        if (commandsRecordInfo.fieldInfo.name) {\n            commandsRecordInfo.withHtmlField = true;\n        }\n        if (this.knowledgeCommandsService.isRecordCompatibleWithMacro(commandsRecordInfo)) {\n            this.knowledgeCommandsService.setCommandsRecordInfo(commandsRecordInfo);\n        }\n    },\n    async onClickSearchKnowledgeArticle() {\n        if (await this.model.root.isDirty() || this.model.root.isNew) {\n            const saved = await this.model.root.save();\n            if (!saved) {\n                return;\n            }\n        }\n        this.command.openMainPalette({ searchValue: \"?\" });\n    },\n};\n\npatch(FormController.prototype, FormControllerPatch);\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class FormErrorDialog extends Component {\n    static template = \"web.FormErrorDialog\";\n    static components = { Dialog };\n    static props = {\n        message: { type: String, optional: true },\n        data: { type: Object },\n        onDiscard: Function,\n        onStayHere: Function,\n        close: Function,\n    };\n\n    setup() {\n        this.action = useService(\"action\");\n        this.message = this.props.message;\n        if (this.props?.data.name === \"odoo.exceptions.RedirectWarning\") {\n            this.message = this.props.data.arguments[0];\n            this.redirectAction = this.props.data.arguments[1];\n            this.redirectBtnLabel = this.props.data.arguments[2];\n        }\n    }\n\n    onRedirectBtnClicked() {\n        this.action.doAction(this.redirectAction);\n        this.stay();\n    }\n\n    async discard() {\n        await this.props.onDiscard();\n        this.props.close();\n    }\n\n    async stay() {\n        await this.props.onStayHere();\n        this.props.close();\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { sortBy } from \"@web/core/utils/arrays\";\n\nclass Group extends Component {\n    static template = \"\";\n    static props = [\"class?\", \"slots?\", \"maxCols?\", \"style?\"];\n    static defaultProps = {\n        maxCols: 2,\n    };\n\n    _getItems() {\n        const items = Object.entries(this.props.slots || {}).filter(([k, v]) => v.type === \"item\");\n        return sortBy(items, (i) => i[1].sequence);\n    }\n\n    getItems() {\n        return this._getItems();\n    }\n\n    get allClasses() {\n        return this.props.class;\n    }\n}\n\nexport class OuterGroup extends Group {\n    static template = \"web.Form.OuterGroup\";\n    static defaultProps = {\n        ...Group.defaultProps,\n        slots: [],\n        hasOuterTemplate: true,\n    };\n\n    getItems() {\n        const nbCols = this.props.maxCols;\n        const colSize = Math.max(1, Math.round(12 / nbCols));\n\n        // Dispatch items across table rows\n        const items = super.getItems().filter(([k, v]) => !(\"isVisible\" in v) || v.isVisible);\n        return items.map((item) => {\n            const [slotName, slot] = item;\n            const itemSpan = slot.itemSpan || 1;\n            return {\n                name: slotName,\n                size: itemSpan * colSize,\n                newline: slot.newline,\n                colspan: itemSpan,\n            };\n        });\n    }\n}\n\nexport class InnerGroup extends Group {\n    static template = \"web.Form.InnerGroup\";\n    getTemplate(subType) {\n        return this.constructor.templates[subType] || this.constructor.templates.default;\n    }\n    getRows() {\n        const maxCols = this.props.maxCols;\n\n        const rows = [];\n        let currentRow = [];\n        let reservedSpace = 0;\n\n        // Dispatch items across table rows\n        const items = this.getItems();\n        while (items.length) {\n            const [slotName, slot] = items.shift();\n            if (!slot.isVisible) {\n                continue;\n            }\n\n            const { newline, itemSpan } = slot;\n            if (newline) {\n                rows.push(currentRow);\n                currentRow = [];\n                reservedSpace = 0;\n            }\n\n            const fullItemSpan = itemSpan || 1;\n\n            if (fullItemSpan + reservedSpace > maxCols) {\n                rows.push(currentRow);\n                currentRow = [];\n                reservedSpace = 0;\n            }\n\n            const isVisible = !(\"isVisible\" in slot) || slot.isVisible;\n            currentRow.push({ ...slot, name: slotName, itemSpan, isVisible });\n            reservedSpace += itemSpan || 1;\n\n            // Allows to remove the line if the content is not visible instead of leaving an empty line.\n            currentRow.isVisible = currentRow.isVisible || isVisible;\n        }\n        rows.push(currentRow);\n\n        return rows;\n    }\n}\n", "import { fieldVisualFeedback } from \"@web/views/fields/field\";\nimport { session } from \"@web/session\";\nimport { getTooltipInfo } from \"@web/views/fields/field_tooltip\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Component } from \"@odoo/owl\";\n\nexport class FormLabel extends Component {\n    static template = \"web.FormLabel\";\n    static props = {\n        fieldInfo: { type: Object },\n        record: { type: Object },\n        fieldName: { type: String },\n        className: { type: String, optional: true },\n        string: { type: String },\n        id: { type: String },\n        notMuttedLabel: { type: Boolean, optional: true },\n    };\n\n    get className() {\n        const { invalid, empty, readonly } = fieldVisualFeedback(\n            this.props.fieldInfo.field,\n            this.props.record,\n            this.props.fieldName,\n            this.props.fieldInfo\n        );\n        const classes = this.props.className ? [this.props.className] : [];\n        if (invalid) {\n            classes.push(\"o_field_invalid\");\n        }\n        if (empty) {\n            classes.push(\"o_form_label_empty\");\n        }\n        if (readonly && !this.props.notMuttedLabel) {\n            classes.push(\"o_form_label_readonly\");\n        }\n        return classes.join(\" \");\n    }\n\n    get hasTooltip() {\n        return Boolean(odoo.debug) || this.tooltipHelp;\n    }\n\n    get tooltipHelp() {\n        const field = this.props.record.fields[this.props.fieldName];\n        let help = this.props.fieldInfo.help || field.help || \"\";\n        if (field.company_dependent && session.display_switch_company_menu) {\n            help += (help ? \"\\n\\n\" : \"\") + _t(\"Values set here are company-specific.\");\n        }\n        return help;\n    }\n    get tooltipInfo() {\n        if (!odoo.debug) {\n            return JSON.stringify({\n                field: {\n                    help: this.tooltipHelp,\n                },\n            });\n        }\n        return getTooltipInfo({\n            viewMode: \"form\",\n            resModel: this.props.record.resModel,\n            field: this.props.record.fields[this.props.fieldName],\n            fieldInfo: this.props.fieldInfo,\n            help: this.tooltipHelp,\n        });\n    }\n}\n", "import { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { Notebook } from \"@web/core/notebook/notebook\";\nimport { Setting } from \"./setting/setting\";\nimport { Field } from \"@web/views/fields/field\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { hasTouch } from \"@web/core/browser/feature_detection\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useDebounced } from \"@web/core/utils/timing\";\nimport { ButtonBox } from \"@web/views/form/button_box/button_box\";\nimport { InnerGroup, OuterGroup } from \"@web/views/form/form_group/form_group\";\nimport { ViewButton } from \"@web/views/view_button/view_button\";\nimport { useViewCompiler } from \"@web/views/view_compiler\";\nimport { useBounceButton } from \"@web/views/view_hook\";\nimport { Widget } from \"@web/views/widgets/widget\";\nimport { FormCompiler } from \"./form_compiler\";\nimport { FormLabel } from \"./form_label\";\nimport { StatusBarButtons } from \"./status_bar_buttons/status_bar_buttons\";\n\nimport {\n    Component,\n    onMounted,\n    onWillUnmount,\n    useEffect,\n    useSubEnv,\n    useRef,\n    useState,\n    xml,\n} from \"@odoo/owl\";\n\nexport class FormRenderer extends Component {\n    static template = xml`<t t-call=\"{{ templates.FormRenderer }}\" t-call-context=\"{ __comp__: Object.assign(Object.create(this), { this: this }) }\" />`;\n    static components = {\n        Field,\n        FormLabel,\n        ButtonBox,\n        ViewButton,\n        Widget,\n        Notebook,\n        Setting,\n        OuterGroup,\n        InnerGroup,\n        StatusBarButtons,\n    };\n    static props = {\n        archInfo: Object,\n        Compiler: { type: Function, optional: true },\n        record: Object,\n        // Template props : added by the FormCompiler\n        class: { type: String, optional: 1 },\n        translateAlert: { type: [Object, { value: null }], optional: true },\n        onNotebookPageChange: { type: Function, optional: true },\n        activeNotebookPages: { type: Object, optional: true },\n        saveRecord: { type: Function, optional: true },\n        setFieldAsDirty: { type: Function, optional: true },\n        slots: { type: Object, optional: true },\n    };\n    static defaultProps = {\n        activeNotebookPages: {},\n        onNotebookPageChange: () => {},\n    };\n\n    setup() {\n        this.evaluateBooleanExpr = evaluateBooleanExpr;\n        const { archInfo, Compiler, record } = this.props;\n        const templates = { FormRenderer: archInfo.xmlDoc };\n        this.state = useState({}); // Used by Form Compiler\n        this.templates = useViewCompiler(Compiler || FormCompiler, templates);\n        useSubEnv({ model: record.model });\n        useBounceButton(useRef(\"compiled_view_root\"), (target) => {\n            return !record.isInEdition && !!target.closest(\".oe_title, .o_inner_group\");\n        });\n        this.uiService = useService(\"ui\");\n        this.onResize = useDebounced(this.render, 200);\n        onMounted(() => browser.addEventListener(\"resize\", this.onResize));\n        onWillUnmount(() => browser.removeEventListener(\"resize\", this.onResize));\n\n        const { autofocusFieldId } = archInfo;\n        const rootRef = useRef(\"compiled_view_root\");\n        if (this.shouldAutoFocus) {\n            useEffect(\n                (isNew, rootEl) => {\n                    if (!rootEl) {\n                        return;\n                    }\n                    let elementToFocus;\n                    if (isNew) {\n                        const focusableSelectors = [\n                            'input[type=\"text\"]',\n                            \"textarea\",\n                            \"[contenteditable]\",\n                        ];\n                        elementToFocus =\n                            (autofocusFieldId && rootEl.querySelector(`#${autofocusFieldId}`)) ||\n                            rootEl.querySelector(\n                                focusableSelectors\n                                    .map((sel) => `.o_content .o_field_widget ${sel}`)\n                                    .join(\", \")\n                            );\n                    }\n                    if (elementToFocus) {\n                        elementToFocus.focus();\n                    }\n                },\n                () => [this.props.record.isNew, rootRef.el]\n            );\n        }\n\n        if (this.env.inDialog) {\n            // try to ensure ids unicity by temporarily removing similar ids that could already\n            // exist in the DOM (e.g. in a form view displayed below this dialog which contains\n            // same field names as this form view)\n            const fieldNodeIds = Object.keys(this.props.archInfo.fieldNodes);\n            const elementsByNodeIds = {};\n            onMounted(() => {\n                if (!rootRef.el) {\n                    // t-ref is sometimes set on a <t> node, resulting in a null ref (e.g. footer case)\n                    return;\n                }\n                for (const id of fieldNodeIds) {\n                    const els = [...document.querySelectorAll(`[id=${id}]`)].filter(\n                        (el) => !rootRef.el.contains(el)\n                    );\n                    if (els.length) {\n                        els[0].removeAttribute(\"id\");\n                        elementsByNodeIds[id] = els[0];\n                    }\n                }\n            });\n            onWillUnmount(() => {\n                for (const [id, el] of Object.entries(elementsByNodeIds)) {\n                    el.setAttribute(\"id\", id);\n                }\n            });\n        }\n    }\n\n    get shouldAutoFocus() {\n        return !hasTouch() && !this.props.archInfo.disableAutofocus;\n    }\n}\n", "import { Component, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { useBus } from \"@web/core/utils/hooks\";\n\nexport class FormStatusIndicator extends Component {\n    static template = \"web.FormStatusIndicator\";\n    static props = {\n        model: Object,\n        save: Function,\n        discard: Function,\n    };\n\n    setup() {\n        this.state = useState({\n            fieldIsDirty: false,\n        });\n        useBus(\n            this.props.model.bus,\n            \"FIELD_IS_DIRTY\",\n            (ev) => (this.state.fieldIsDirty = ev.detail)\n        );\n        useEffect(\n            () => {\n                if (!this.props.model.root.isNew && this.indicatorMode === \"invalid\") {\n                    this.saveButton.el.setAttribute(\"disabled\", \"1\");\n                } else {\n                    this.saveButton.el.removeAttribute(\"disabled\");\n                }\n            },\n            () => [this.props.model.root.isValid]\n        );\n\n        this.saveButton = useRef(\"save\");\n    }\n\n    get displayButtons() {\n        return this.indicatorMode !== \"saved\";\n    }\n\n    get indicatorMode() {\n        if (this.props.model.root.isNew) {\n            return this.props.model.root.isValid ? \"dirty\" : \"invalid\";\n        } else if (!this.props.model.root.isValid) {\n            return \"invalid\";\n        } else if (this.props.model.root.dirty || this.state.fieldIsDirty) {\n            return \"dirty\";\n        } else {\n            return \"saved\";\n        }\n    }\n\n    async discard() {\n        await this.props.discard();\n    }\n    async save() {\n        await this.props.save();\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { RelationalModel } from \"@web/model/relational_model/relational_model\";\nimport { FormRenderer } from \"./form_renderer\";\nimport { FormArchParser } from \"./form_arch_parser\";\nimport { FormController } from \"./form_controller\";\nimport { FormCompiler } from \"./form_compiler\";\n\nexport const formView = {\n    type: \"form\",\n    searchMenuTypes: [],\n    Controller: FormController,\n    Renderer: FormRenderer,\n    ArchParser: FormArchParser,\n    Model: RelationalModel,\n    Compiler: FormCompiler,\n    buttonTemplate: \"web.FormView.Buttons\",\n\n    props: (genericProps, view) => {\n        const { ArchParser } = view;\n        const { arch, relatedModels, resModel } = genericProps;\n        const archInfo = new ArchParser().parse(arch, relatedModels, resModel);\n\n        return {\n            ...genericProps,\n            Model: view.Model,\n            Renderer: view.Renderer,\n            buttonTemplate: genericProps.buttonTemplate || view.buttonTemplate,\n            Compiler: view.Compiler,\n            archInfo,\n        };\n    },\n};\n\nregistry.category(\"views\").add(\"form\", formView);\n", "import { session } from \"@web/session\";\nimport { Component } from \"@odoo/owl\";\nimport { FormLabel } from \"../form_label\";\nimport { DocumentationLink } from \"@web/views/widgets/documentation_link/documentation_link\";\n\nexport class Setting extends Component {\n    static template = \"web.Setting\";\n    static components = {\n        FormLabel,\n        DocumentationLink,\n    };\n    static props = {\n        id: { type: String, optional: 1 },\n        title: { type: String, optional: 1 },\n        fieldId: { type: String, optional: 1 },\n        help: { type: String, optional: 1 },\n        fieldName: { type: String, optional: 1 },\n        fieldInfo: { type: Object, optional: 1 },\n        class: { type: String, optional: 1 },\n        record: { type: Object, optional: 1 },\n        documentation: { type: String, optional: 1 },\n        string: { type: String, optional: 1 },\n        addLabel: { type: Boolean },\n        companyDependent: { type: Boolean, optional: 1 },\n        slots: { type: Object, optional: 1 },\n    };\n\n    setup() {\n        if (this.props.fieldName) {\n            this.fieldType = this.props.record.fields[this.props.fieldName].type;\n            if (this.props.fieldInfo.readonly === \"True\") {\n                this.notMuttedLabel = true;\n            }\n        }\n    }\n\n    get classNames() {\n        const { class: _class } = this.props;\n        const classNames = {\n            o_setting_box: true,\n            \"col-12\": true,\n            \"col-lg-6\": true,\n            [_class]: Boolean(_class),\n        };\n\n        return classNames;\n    }\n\n    get displayCompanyDependentIcon() {\n        return (\n            this.labelString && this.props.companyDependent && session.display_switch_company_menu\n        );\n    }\n\n    get labelString() {\n        if (this.props.string) {\n            return this.props.string;\n        }\n        const label =\n            this.props.record &&\n            this.props.record.fields[this.props.fieldName] &&\n            this.props.record.fields[this.props.fieldName].string;\n        return label || \"\";\n    }\n}\n", "import { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class StatusBarButtons extends Component {\n    static template = \"web.StatusBarButtons\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n    };\n    static props = {\n        slots: { type: Object, optional: 1 },\n    };\n\n    get visibleSlotNames() {\n        if (!this.props.slots) {\n            return [];\n        }\n        return Object.entries(this.props.slots)\n            .filter((entry) => entry[1].isVisible)\n            .map((entry) => entry[0]);\n    }\n}\n", "import { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\n\nimport { StatusBarButtons } from \"../status_bar_buttons/status_bar_buttons\";\n\nexport class StatusBarDropdownItems extends StatusBarButtons {\n    static template = \"web.StatusBarDropdownItems\";\n    static components = {\n        DropdownItem,\n    };\n}\n", "import { exprToBoolean } from \"@web/core/utils/strings\";\nimport { extractAttributes, visitXML } from \"@web/core/utils/xml\";\nimport { stringToOrderBy } from \"@web/search/utils/order_by\";\nimport { Field } from \"@web/views/fields/field\";\nimport { getActiveActions, processButton } from \"@web/views/utils\";\nimport { Widget } from \"@web/views/widgets/widget\";\n\n/**\n * NOTE ON 't-name=\"kanban-box\"':\n *\n * \"kanban-box\" is deprecated. Kanban archs converted to the new (v18) API must\n * define a \"card\" template instead.\n *\n * Multiple roots are supported in kanban box template definitions, however there\n * are a few things to keep in mind when doing so:\n *\n * - each root will generate its own card, so it would be preferable to make the\n * roots mutually exclusive to avoid rendering multiple cards for the same record;\n *\n * - certain fields such as the kanban 'color' or the 'handle' field are based on\n * the last encountered node, so it is advised to keep the same values for those\n * fields within all roots to avoid inconsistencies.\n */\n\nexport const LEGACY_KANBAN_BOX_ATTRIBUTE = \"kanban-box\";\nexport const LEGACY_KANBAN_MENU_ATTRIBUTE = \"kanban-menu\";\nexport const KANBAN_CARD_ATTRIBUTE = \"card\";\nexport const KANBAN_MENU_ATTRIBUTE = \"menu\";\n\nexport class KanbanArchParser {\n    parse(xmlDoc, models, modelName) {\n        const fields = models[modelName].fields;\n        const className = xmlDoc.getAttribute(\"class\") || null;\n        const canOpenRecords = exprToBoolean(xmlDoc.getAttribute(\"can_open\"), true);\n        let defaultOrder = stringToOrderBy(xmlDoc.getAttribute(\"default_order\") || null);\n        const defaultGroupBy = xmlDoc.getAttribute(\"default_group_by\");\n        const limit = xmlDoc.getAttribute(\"limit\");\n        const countLimit = xmlDoc.getAttribute(\"count_limit\");\n        const recordsDraggable = exprToBoolean(xmlDoc.getAttribute(\"records_draggable\"), true);\n        const groupsDraggable = exprToBoolean(xmlDoc.getAttribute(\"groups_draggable\"), true);\n        const activeActions = getActiveActions(xmlDoc);\n        activeActions.archiveGroup = exprToBoolean(xmlDoc.getAttribute(\"archivable\"), true);\n        activeActions.createGroup = exprToBoolean(xmlDoc.getAttribute(\"group_create\"), true);\n        activeActions.deleteGroup = exprToBoolean(xmlDoc.getAttribute(\"group_delete\"), true);\n        activeActions.editGroup = exprToBoolean(xmlDoc.getAttribute(\"group_edit\"), true);\n        activeActions.quickCreate =\n            activeActions.create && exprToBoolean(xmlDoc.getAttribute(\"quick_create\"), true);\n        const onCreate = xmlDoc.getAttribute(\"on_create\");\n        const quickCreateView = xmlDoc.getAttribute(\"quick_create_view\");\n        const tooltipInfo = {};\n        let handleField = null;\n        const fieldNodes = {};\n        const fieldNextIds = {};\n        const widgetNodes = {};\n        let widgetNextId = 0;\n        const jsClass = xmlDoc.getAttribute(\"js_class\");\n        const action = xmlDoc.getAttribute(\"action\");\n        const type = xmlDoc.getAttribute(\"type\");\n        const openAction = action && type ? { action, type } : null;\n        const templateDocs = {};\n        let headerButtons = [];\n        const creates = [];\n        let button_id = 0;\n        // Root level of the template\n        visitXML(xmlDoc, (node) => {\n            if (node.hasAttribute(\"t-name\")) {\n                templateDocs[node.getAttribute(\"t-name\")] = node;\n                return;\n            }\n            if (node.tagName === \"header\") {\n                headerButtons = [...node.children]\n                    .filter((node) => node.tagName === \"button\")\n                    .map((node) => ({\n                        ...processButton(node),\n                        type: \"button\",\n                        id: button_id++,\n                    }))\n                    .filter((button) => button.invisible !== \"True\" && button.invisible !== \"1\");\n                return false;\n            } else if (node.tagName === \"control\") {\n                for (const childNode of node.children) {\n                    if (childNode.tagName === \"button\") {\n                        creates.push({\n                            type: \"button\",\n                            ...processButton(childNode),\n                        });\n                    } else if (childNode.tagName === \"create\") {\n                        creates.push({\n                            type: \"create\",\n                            context: childNode.getAttribute(\"context\"),\n                            string: childNode.getAttribute(\"string\"),\n                        });\n                    }\n                }\n                return false;\n            }\n            // Case: field node\n            if (node.tagName === \"field\") {\n                // In kanban, we display many2many fields as tags by default\n                const widget = node.getAttribute(\"widget\");\n                if (\n                    !widget &&\n                    models[modelName].fields[node.getAttribute(\"name\")].type === \"many2many\"\n                ) {\n                    node.setAttribute(\"widget\", \"many2many_tags\");\n                }\n                const fieldInfo = Field.parseFieldNode(node, models, modelName, \"kanban\", jsClass);\n                const name = fieldInfo.name;\n                if (!(fieldInfo.name in fieldNextIds)) {\n                    fieldNextIds[fieldInfo.name] = 0;\n                }\n                const fieldId = `${fieldInfo.name}_${fieldNextIds[fieldInfo.name]++}`;\n                fieldNodes[fieldId] = fieldInfo;\n                node.setAttribute(\"field_id\", fieldId);\n                if (fieldInfo.options.group_by_tooltip) {\n                    tooltipInfo[name] = fieldInfo.options.group_by_tooltip;\n                }\n                if (fieldInfo.isHandle) {\n                    handleField = name;\n                }\n            }\n            if (node.tagName === \"widget\") {\n                const widgetInfo = Widget.parseWidgetNode(node);\n                const widgetId = `widget_${++widgetNextId}`;\n                widgetNodes[widgetId] = widgetInfo;\n                node.setAttribute(\"widget_id\", widgetId);\n            }\n\n            // Keep track of last update so images can be reloaded when they may have changed.\n            if (node.tagName === \"img\") {\n                const attSrc = node.getAttribute(\"t-att-src\");\n                if (\n                    attSrc &&\n                    /\\bkanban_image\\b/.test(attSrc) &&\n                    !Object.values(fieldNodes).some((f) => f.name === \"write_date\")\n                ) {\n                    fieldNodes.write_date_0 = { name: \"write_date\", type: \"datetime\" };\n                }\n            }\n        });\n\n        // Progressbar\n        let progressAttributes = false;\n        const progressBar = xmlDoc.querySelector(\"progressbar\");\n        if (progressBar) {\n            progressAttributes = this.parseProgressBar(progressBar, fields);\n        }\n\n        // Concrete kanban box elements in the template\n        let cardDoc = templateDocs[KANBAN_CARD_ATTRIBUTE];\n        const isLegacyArch = !cardDoc;\n        if (isLegacyArch) {\n            console.warn(\"'kanban-box' is deprecated, define a 'card' template instead\");\n        }\n        if (!cardDoc) {\n            cardDoc = templateDocs[LEGACY_KANBAN_BOX_ATTRIBUTE];\n            if (!cardDoc) {\n                throw new Error(`Missing '${KANBAN_CARD_ATTRIBUTE}' template.`);\n            }\n        }\n        const cardClassName = (!isLegacyArch && cardDoc.getAttribute(\"class\")) || \"\";\n\n        if (!defaultOrder.length && handleField) {\n            const handleFieldSort = `${handleField}, id`;\n            defaultOrder = stringToOrderBy(handleFieldSort);\n        }\n\n        return {\n            activeActions,\n            canOpenRecords,\n            cardClassName,\n            cardColorField: xmlDoc.getAttribute(\"highlight_color\"),\n            className,\n            creates,\n            defaultGroupBy,\n            fieldNodes,\n            widgetNodes,\n            handleField,\n            headerButtons,\n            defaultOrder,\n            onCreate,\n            openAction,\n            quickCreateView,\n            recordsDraggable,\n            groupsDraggable,\n            limit: limit && parseInt(limit, 10),\n            countLimit: countLimit && parseInt(countLimit, 10),\n            progressAttributes,\n            templateDocs,\n            tooltipInfo,\n            examples: xmlDoc.getAttribute(\"examples\"),\n            xmlDoc,\n            isLegacyArch,\n        };\n    }\n\n    parseProgressBar(progressBar, fields) {\n        const attrs = extractAttributes(progressBar, [\"field\", \"colors\", \"sum_field\", \"help\"]);\n        return {\n            fieldName: attrs.field,\n            colors: JSON.parse(attrs.colors),\n            sumField: fields[attrs.sum_field] || false,\n            help: attrs.help,\n        };\n    }\n}\n", "import { ColorList } from \"@web/core/colorlist/colorlist\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { createElement } from \"@web/core/utils/xml\";\n\nimport { LEGACY_KANBAN_BOX_ATTRIBUTE, KanbanArchParser } from \"./kanban_arch_parser\";\nimport { KanbanCompiler } from \"./kanban_compiler\";\nimport { KanbanRecord, getColorIndex } from \"./kanban_record\";\n\n// TODO: remove this backward compatibility layer post v18\n\npatch(KanbanArchParser.prototype, {\n    parse(xmlDoc, models, modelName) {\n        const archInfo = super.parse(xmlDoc, models, modelName);\n\n        // Color and color picker (first node found is taken for each)\n        const legacyCardDoc = archInfo.templateDocs[LEGACY_KANBAN_BOX_ATTRIBUTE];\n        if (legacyCardDoc) {\n            const cardColorEl = legacyCardDoc.querySelector(\"[color]\");\n            const cardColorField = cardColorEl && cardColorEl.getAttribute(\"color\");\n\n            const colorEl = xmlDoc.querySelector(\"templates .oe_kanban_colorpicker[data-field]\");\n            const colorField = (colorEl && colorEl.getAttribute(\"data-field\")) || \"color\";\n\n            archInfo.cardColorField = archInfo.cardColorField || cardColorField;\n            archInfo.colorField = colorField;\n        }\n\n        return archInfo;\n    },\n});\n\npatch(KanbanCompiler.prototype, {\n    setup() {\n        super.setup();\n        this.compilers.push({ selector: \".oe_kanban_colorpicker\", fn: this.compileColorPicker });\n    },\n\n    /**\n     * @returns {Element}\n     */\n    compileColorPicker() {\n        return createElement(\"t\", {\n            \"t-call\": \"web.KanbanColorPicker\",\n            \"t-call-context\": \"__comp__\",\n        });\n    },\n});\n\n/**\n * Returns the class name of a record according to its color.\n */\nfunction getColorClass(value) {\n    return `oe_kanban_color_${getColorIndex(value)}`;\n}\n\n/**\n * Returns the proper translated name of a record color.\n */\nfunction getColorName(value) {\n    return ColorList.COLORS[getColorIndex(value)];\n}\n\npatch(KanbanRecord.prototype, {\n    selectColor(colorIndex) {\n        const { archInfo, record } = this.props;\n        return record.update({ [archInfo.colorField]: colorIndex }, { save: true });\n    },\n\n    get renderingContext() {\n        return Object.assign(super.renderingContext, {\n            kanban_color: getColorClass,\n            kanban_getcolor: getColorIndex,\n            kanban_getcolorname: getColorName,\n        });\n    },\n});\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { Notebook } from \"@web/core/notebook/notebook\";\n\nimport { Component, useRef } from \"@odoo/owl\";\n\nconst random = (min, max) => Math.floor(Math.random() * (max - min) + min);\n\nclass KanbanExamplesNotebookTemplate extends Component {\n    static template = \"web.KanbanExamplesNotebookTemplate\";\n    static props = [\"*\"];\n    static defaultProps = {\n        columns: [],\n        foldedColumns: [],\n    };\n    setup() {\n        this.columns = [];\n        const hasBullet = this.props.bullets && this.props.bullets.length;\n        const allColumns = [...this.props.columns, ...this.props.foldedColumns];\n        for (const title of allColumns) {\n            const col = { title, records: [] };\n            this.columns.push(col);\n            for (let i = 0; i < random(1, 5); i++) {\n                const rec = { id: i };\n                if (hasBullet && Math.random() > 0.3) {\n                    const sampleId = Math.floor(Math.random() * this.props.bullets.length);\n                    rec.bullet = this.props.bullets[sampleId];\n                }\n                col.records.push(rec);\n            }\n        }\n    }\n}\n\nexport class KanbanColumnExamplesDialog extends Component {\n    static template = \"web.KanbanColumnExamplesDialog\";\n    static components = { Dialog, Notebook };\n    static props = [\"*\"];\n\n    setup() {\n        this.navList = useRef(\"navList\");\n        this.pages = [];\n        this.activePage = null;\n        this.props.examples.forEach((eg) => {\n            this.pages.push({\n                Component: KanbanExamplesNotebookTemplate,\n                title: eg.name,\n                props: eg,\n                id: eg.name,\n            });\n        });\n    }\n\n    onPageUpdate(page) {\n        this.activePage = page;\n    }\n\n    applyExamples() {\n        const index = this.props.examples.findIndex((e) => e.name === this.activePage);\n        this.props.applyExamples(index);\n        this.props.close();\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { useAutofocus, useService } from \"@web/core/utils/hooks\";\nimport { KanbanColumnExamplesDialog } from \"./kanban_column_examples_dialog\";\n\nimport { Component, useExternalListener, useState, useRef } from \"@odoo/owl\";\n\nexport class KanbanColumnQuickCreate extends Component {\n    static template = \"web.KanbanColumnQuickCreate\";\n    static props = {\n        exampleData: [Object, { value: null }],\n        onFoldChange: Function,\n        onValidate: Function,\n        folded: Boolean,\n        groupByField: Object,\n    };\n\n    setup() {\n        this.dialog = useService(\"dialog\");\n        this.root = useRef(\"root\");\n        this.state = useState({\n            hasInputFocused: false,\n        });\n\n        useAutofocus();\n        this.inputRef = useRef(\"autofocus\");\n\n        // Close on outside click\n        useExternalListener(window, \"mousedown\", (/** @type {MouseEvent} */ ev) => {\n            // This target is kept in order to impeach close on outside click behavior if the click\n            // has been initiated from the quickcreate root element (mouse selection in an input...)\n            this.mousedownTarget = ev.target;\n        });\n        useExternalListener(\n            window,\n            \"click\",\n            (/** @type {MouseEvent} */ ev) => {\n                const target = this.mousedownTarget || ev.target;\n                const gotClickedInside = this.root.el.contains(target);\n                if (!gotClickedInside) {\n                    this.fold();\n                }\n                this.mousedownTarget = null;\n            },\n            { capture: true }\n        );\n\n        // Key Navigation\n        useHotkey(\"escape\", () => this.fold());\n    }\n\n    get canShowExamples() {\n        const { allowedGroupBys = [], examples = [] } = this.props.exampleData || {};\n        const hasExamples = Boolean(examples.length);\n        return hasExamples && allowedGroupBys.includes(this.props.groupByField.name);\n    }\n\n    get relatedFieldName() {\n        return this.props.groupByField.string;\n    }\n\n    fold() {\n        this.props.onFoldChange(true);\n    }\n\n    unfold() {\n        this.props.onFoldChange(false);\n    }\n\n    validate() {\n        const title = this.inputRef.el.value.trim();\n        if (title.length) {\n            this.props.onValidate(title);\n            this.inputRef.el.value = \"\";\n        }\n    }\n\n    showExamples() {\n        this.dialog.add(KanbanColumnExamplesDialog, {\n            examples: this.props.exampleData.examples,\n            applyExamplesText:\n                this.props.exampleData.applyExamplesText || _t(\"Use This For My Kanban\"),\n            applyExamples: (index) => {\n                const { examples, foldField } = this.props.exampleData;\n                const { columns, foldedColumns = [] } = examples[index];\n                for (const groupName of columns) {\n                    this.props.onValidate(groupName);\n                }\n                for (const groupName of foldedColumns) {\n                    this.props.onValidate(groupName, foldField);\n                }\n            },\n        });\n    }\n\n    onInputKeydown(ev) {\n        if (ev.key === \"Enter\") {\n            this.validate();\n        }\n    }\n}\n", "import {\n    append,\n    combineAttributes,\n    createElement,\n    extractAttributes,\n    getTag,\n} from \"@web/core/utils/xml\";\nimport { toStringExpression } from \"@web/views/utils\";\nimport { toInterpolatedStringExpression, ViewCompiler } from \"@web/views/view_compiler\";\n\n/**\n * @typedef {Object} DropdownDef\n * @property {Element} el\n * @property {boolean} inserted\n * @property {boolean} shouldInsert\n * @property {(\"dropdown\" | \"toggler\" | \"menu\")[]} parts\n */\n\nconst ACTION_TYPES = [\"action\", \"object\"];\nconst SPECIAL_TYPES = [\n    ...ACTION_TYPES,\n    \"edit\",\n    \"open\",\n    \"delete\",\n    \"url\",\n    \"set_cover\",\n    \"archive\",\n    \"unarchive\",\n];\n\nexport class KanbanCompiler extends ViewCompiler {\n    setup() {\n        this.ctx.readonly = \"read_only_mode\";\n        this.compilers.push(\n            { selector: \"t[t-call]\", fn: this.compileTCall },\n            { selector: \"img\", fn: this.compileImage }\n        );\n    }\n\n    //-----------------------------------------------------------------------------\n    // Compilers\n    //-----------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    compileButton(el, params) {\n        const type = el.getAttribute(\"type\");\n        if (!SPECIAL_TYPES.includes(type)) {\n            // Not a kanban-specific action type.\n            return super.compileButton(el, params);\n        }\n\n        combineAttributes(el, \"class\", [\"oe_kanban_action\"]);\n\n        if (ACTION_TYPES.includes(type)) {\n            if (!el.hasAttribute(\"debounce\")) {\n                // action buttons are debounced in kanban records\n                el.setAttribute(\"debounce\", 300);\n            }\n            return super.compileButton(el, params);\n        }\n\n        const nodeParams = extractAttributes(el, [\"type\"]);\n        if (type === \"set_cover\") {\n            const { \"auto-open\": autoOpen, \"data-field\": fieldName } = extractAttributes(el, [\n                \"auto-open\",\n                \"data-field\",\n            ]);\n            Object.assign(nodeParams, { autoOpen, fieldName });\n        }\n        const strParams = Object.entries(nodeParams)\n            .map(([k, v]) => [k, toStringExpression(v)].join(\":\"))\n            .join(\",\");\n        el.setAttribute(\"t-on-click\", `()=>__comp__.triggerAction({${strParams}})`);\n\n        const compiled = createElement(el.nodeName);\n        for (const { name, value } of el.attributes) {\n            compiled.setAttribute(name, value);\n        }\n        if (getTag(el, true) === \"a\" && !compiled.hasAttribute(\"href\")) {\n            compiled.setAttribute(\"href\", \"#\");\n        }\n        for (const child of el.childNodes) {\n            append(compiled, this.compileNode(child, params));\n        }\n\n        return compiled;\n    }\n    /**\n     * @returns {Element}\n     */\n    compileImage(el) {\n        const element = el.cloneNode(true);\n        element.setAttribute(\"loading\", \"lazy\");\n        return element;\n    }\n\n    /**\n     * @override\n     */\n    compileField(el, params) {\n        let compiled;\n        let isSpan = false;\n        const recordExpr = params.recordExpr || \"__comp__.props.record\";\n        const dataPointIdExpr = params.dataPointIdExpr || `${recordExpr}.id`;\n        if (!el.hasAttribute(\"widget\")) {\n            isSpan = true;\n            // fields without a specified widget are rendered as simple spans in kanban records\n            const fieldId = el.getAttribute(\"field_id\");\n            compiled = createElement(\"span\", {\n                \"t-out\": params.formattedValueExpr || `__comp__.getFormattedValue(\"${fieldId}\")`,\n            });\n        } else {\n            compiled = super.compileField(el, params);\n            const fieldId = el.getAttribute(\"field_id\");\n            compiled.setAttribute(\"id\", `'${fieldId}_' + ${dataPointIdExpr}`);\n            // In x2many kanban, records can be edited in a dialog. The same record as the one of\n            // the kanban is used for the form view dialog, so its mode is switched to \"edit\", but\n            // we don't want to see it in edition in the background. For that reason, we force its\n            // fields to be readonly when the record is in edition, i.e. when it is opened in a form\n            // view dialog.\n            const readonlyAttr = compiled.getAttribute(\"readonly\");\n            if (readonlyAttr) {\n                compiled.setAttribute(\"readonly\", `${recordExpr}.isInEdition || (${readonlyAttr})`);\n            } else {\n                compiled.setAttribute(\"readonly\", `${recordExpr}.isInEdition`);\n            }\n        }\n\n        if (params.isLegacy) {\n            const { bold, display } = extractAttributes(el, [\"bold\", \"display\"]);\n            const classNames = [];\n            if (display === \"right\") {\n                classNames.push(\"float-end\");\n            } else if (display === \"full\") {\n                classNames.push(\"o_text_block\");\n            }\n            if (bold) {\n                classNames.push(\"o_text_bold\");\n            }\n            if (classNames.length > 0) {\n                const clsFormatted = isSpan\n                    ? classNames.join(\" \")\n                    : toStringExpression(classNames.join(\" \"));\n                compiled.setAttribute(\"class\", clsFormatted);\n            }\n        }\n\n        const attrs = {};\n        for (const attr of el.attributes) {\n            attrs[attr.name] = attr.value;\n        }\n\n        if (el.hasAttribute(\"widget\")) {\n            const attrsParts = Object.entries(attrs).map(([key, value]) => {\n                if (key.startsWith(\"t-attf-\")) {\n                    key = key.slice(7);\n                    value = toInterpolatedStringExpression(value);\n                } else if (key.startsWith(\"t-att-\")) {\n                    key = key.slice(6);\n                    value = `\"\" + (${value})`;\n                } else if (key.startsWith(\"t-att\")) {\n                    throw new Error(\"t-att on <field> nodes is not supported\");\n                } else if (!key.startsWith(\"t-\")) {\n                    value = toStringExpression(value);\n                }\n                return `'${key}':${value}`;\n            });\n            compiled.setAttribute(\"attrs\", `{${attrsParts.join(\",\")}}`);\n        }\n\n        for (const attr in attrs) {\n            if (attr.startsWith(\"t-\") && !attr.startsWith(\"t-att\")) {\n                compiled.setAttribute(attr, attrs[attr]);\n            }\n        }\n\n        return compiled;\n    }\n\n    /**\n     * @param {Element} el\n     * @param {Object} params\n     * @returns {Element}\n     */\n    compileTCall(el, params) {\n        const compiled = this.compileGenericNode(el, params);\n        const tname = el.getAttribute(\"t-call\");\n        if (tname in this.templates) {\n            compiled.setAttribute(\"t-call\", `{{__comp__.templates[${toStringExpression(tname)}]}}`);\n        }\n        return compiled;\n    }\n}\nKanbanCompiler.OWL_DIRECTIVE_WHITELIST = [\n    ...ViewCompiler.OWL_DIRECTIVE_WHITELIST,\n    \"t-name\",\n    \"t-esc\",\n    \"t-out\",\n    \"t-set\",\n    \"t-value\",\n    \"t-if\",\n    \"t-else\",\n    \"t-elif\",\n    \"t-foreach\",\n    \"t-as\",\n    \"t-key\",\n    \"t-att.*\",\n    \"t-call\",\n    \"t-translation\",\n];\n", "import {\n    deleteConfirmationMessage,\n    ConfirmationDialog,\n} from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { CogMenu } from \"@web/search/cog_menu/cog_menu\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { useSetupAction } from \"@web/search/action_hook\";\nimport { Layout } from \"@web/search/layout\";\nimport { usePager } from \"@web/search/pager_hook\";\nimport { SearchBar } from \"@web/search/search_bar/search_bar\";\nimport { useSearchBarToggler } from \"@web/search/search_bar/search_bar_toggler\";\nimport { session } from \"@web/session\";\nimport { useModelWithSampleData } from \"@web/model/model\";\nimport { standardViewProps } from \"@web/views/standard_view_props\";\nimport { MultiRecordViewButton } from \"@web/views/view_button/multi_record_view_button\";\nimport { useViewButtons } from \"@web/views/view_button/view_button_hook\";\nimport { addFieldDependencies, extractFieldsFromArchInfo } from \"@web/model/relational_model/utils\";\nimport { KanbanRenderer } from \"./kanban_renderer\";\nimport { useProgressBar } from \"./progress_bar_hook\";\n\nimport { Component, reactive, useRef, useState } from \"@odoo/owl\";\n\nconst QUICK_CREATE_FIELD_TYPES = [\"char\", \"boolean\", \"many2one\", \"selection\", \"many2many\"];\n\n// -----------------------------------------------------------------------------\n\nexport class KanbanController extends Component {\n    static template = `web.KanbanView`;\n    static components = { Layout, KanbanRenderer, MultiRecordViewButton, SearchBar, CogMenu };\n    static props = {\n        ...standardViewProps,\n        defaultGroupBy: {\n            validate: (dgb) => !dgb || typeof dgb === \"string\",\n            optional: true,\n        },\n        editable: { type: Boolean, optional: true },\n        forceGlobalClick: { type: Boolean, optional: true },\n        onSelectionChanged: { type: Function, optional: true },\n        showButtons: { type: Boolean, optional: true },\n        Compiler: { type: Function, optional: true }, // optional in stable for backward compatibility\n        Model: Function,\n        Renderer: Function,\n        buttonTemplate: String,\n        archInfo: Object,\n    };\n\n    static defaultProps = {\n        createRecord: () => {},\n        forceGlobalClick: false,\n        selectRecord: () => {},\n        showButtons: true,\n    };\n\n    setup() {\n        this.actionService = useService(\"action\");\n        this.dialog = useService(\"dialog\");\n        const { Model, archInfo } = this.props;\n\n        class KanbanSampleModel extends Model {\n            setup() {\n                super.setup(...arguments);\n                this.initialSampleGroups = undefined;\n            }\n\n            /**\n             * @override\n             */\n            hasData() {\n                if (this.root.groups) {\n                    if (!this.root.groups.length) {\n                        // While we don't have any data, we want to display the column quick create and\n                        // example background. Return true so that we don't get sample data instead\n                        return true;\n                    }\n                    return this.root.groups.some((group) => group.hasData);\n                }\n                return super.hasData();\n            }\n\n            async load() {\n                if (this.orm.isSample && this.initialSampleGroups) {\n                    this.orm.setGroups(this.initialSampleGroups);\n                }\n                return super.load(...arguments);\n            }\n\n            async _webReadGroup() {\n                const result = await super._webReadGroup(...arguments);\n                if (!this.initialSampleGroups) {\n                    this.initialSampleGroups = JSON.parse(JSON.stringify(result.groups));\n                }\n                return result;\n            }\n\n            removeSampleDataInGroups() {\n                if (this.useSampleModel) {\n                    for (const group of this.root.groups) {\n                        const list = group.list;\n                        group.count = 0;\n                        list.count = 0;\n                        if (list.records) {\n                            list.records = [];\n                        } else {\n                            list.groups = [];\n                        }\n                    }\n                }\n            }\n        }\n\n        this.model = useState(\n            useModelWithSampleData(KanbanSampleModel, this.modelParams, this.modelOptions)\n        );\n        if (archInfo.progressAttributes) {\n            const { activeBars } = this.props.state || {};\n            this.progressBarState = useProgressBar(\n                archInfo.progressAttributes,\n                this.model,\n                this.progressBarAggregateFields,\n                activeBars\n            );\n        }\n        this.headerButtons = archInfo.headerButtons;\n\n        const self = this;\n        this.quickCreateState = reactive({\n            get groupId() {\n                return this._groupId || false;\n            },\n            set groupId(groupId) {\n                if (self.model.useSampleModel) {\n                    self.model.removeSampleDataInGroups();\n                    self.model.useSampleModel = false;\n                }\n                this._groupId = groupId;\n            },\n            view: archInfo.quickCreateView,\n        });\n\n        this.rootRef = useRef(\"root\");\n        useViewButtons(this.rootRef, {\n            beforeExecuteAction: this.beforeExecuteActionButton.bind(this),\n            afterExecuteAction: this.afterExecuteActionButton.bind(this),\n            reload: () => this.model.load(),\n        });\n        useSetupAction({\n            rootRef: this.rootRef,\n            beforeLeave: () =>\n                // wait for potential pending write operations (e.g. records being moved)\n                this.model.mutex.getUnlockedDef(),\n            getLocalState: () => ({\n                activeBars: this.progressBarState?.activeBars,\n                modelState: this.model.exportState(),\n            }),\n        });\n        usePager(() => {\n            const root = this.model.root;\n            const { count, hasLimitedCount, isGrouped, limit, offset } = root;\n            if (!isGrouped && !this.model.useSampleModel) {\n                return {\n                    offset: offset,\n                    limit: limit,\n                    total: count,\n                    onUpdate: async ({ offset, limit }, hasNavigated) => {\n                        await this.model.root.load({ offset, limit });\n                        await this.onUpdatedPager();\n                        if (hasNavigated) {\n                            this.onPageChangeScroll();\n                        }\n                    },\n                    updateTotal: hasLimitedCount ? () => root.fetchCount() : undefined,\n                };\n            }\n        });\n        this.searchBarToggler = useSearchBarToggler();\n    }\n\n    get modelParams() {\n        const { resModel, archInfo, limit, defaultGroupBy } = this.props;\n        const { activeFields, fields } = extractFieldsFromArchInfo(archInfo, this.props.fields);\n\n        const cardColorField = archInfo.cardColorField;\n        if (cardColorField) {\n            addFieldDependencies(activeFields, fields, [{ name: cardColorField, type: \"integer\" }]);\n        }\n\n        // Remove fields aggregator unused to avoid asking them for no reason\n        const aggregateFieldNames = this.progressBarAggregateFields.map((field) => field.name);\n        for (const [key, value] of Object.entries(activeFields)) {\n            if (!aggregateFieldNames.includes(key)) {\n                value.aggregator = null;\n            }\n        }\n\n        addFieldDependencies(activeFields, fields, this.progressBarAggregateFields);\n        const modelConfig = this.props.state?.modelState?.config || {\n            resModel,\n            activeFields,\n            fields,\n            openGroupsByDefault: true,\n        };\n\n        return {\n            config: modelConfig,\n            state: this.props.state?.modelState,\n            limit: archInfo.limit || limit || 40,\n            groupsLimit: Number.MAX_SAFE_INTEGER, // no limit\n            countLimit: archInfo.countLimit,\n            defaultGroupBy,\n            defaultOrderBy: archInfo.defaultOrder,\n            maxGroupByDepth: 1,\n            activeIdsLimit: session.active_ids_limit,\n            hooks: {\n                onRecordSaved: this.onRecordSaved.bind(this),\n            },\n        };\n    }\n\n    get modelOptions() {\n        return {};\n    }\n\n    get progressBarAggregateFields() {\n        const res = [];\n        const { progressAttributes } = this.props.archInfo;\n        if (progressAttributes && progressAttributes.sumField) {\n            res.push(progressAttributes.sumField);\n        }\n        return res;\n    }\n\n    get className() {\n        if (this.env.isSmall && this.model.root.isGrouped) {\n            const classList = (this.props.className || \"\").split(\" \");\n            classList.push(\"o_action_delegate_scroll\");\n            return classList.join(\" \");\n        }\n        return this.props.className;\n    }\n\n    async deleteRecord(record) {\n        this.dialog.add(ConfirmationDialog, {\n            title: _t(\"Bye-bye, record!\"),\n            body: deleteConfirmationMessage,\n            confirm: () => this.model.root.deleteRecords([record]),\n            confirmLabel: _t(\"Delete\"),\n            cancel: () => {},\n            cancelLabel: _t(\"No, keep it\"),\n        });\n    }\n\n    evalViewModifier(modifier) {\n        return evaluateBooleanExpr(modifier, { context: this.props.context });\n    }\n\n    async openRecord(record, mode) {\n        const activeIds = this.model.root.records.map((datapoint) => datapoint.resId);\n        this.props.selectRecord(record.resId, { activeIds, mode });\n    }\n\n    async createRecord() {\n        const { onCreate } = this.props.archInfo;\n        const { root } = this.model;\n        if (this.canQuickCreate && onCreate === \"quick_create\") {\n            const firstGroup = root.groups[0];\n            if (firstGroup.isFolded) {\n                await firstGroup.toggle();\n            }\n            this.quickCreateState.groupId = firstGroup.id;\n        } else if (onCreate && onCreate !== \"quick_create\") {\n            const options = {\n                additionalContext: root.context,\n                onClose: async () => {\n                    await root.load();\n                    this.model.useSampleModel = false;\n                    this.render(true); // FIXME WOWL reactivity\n                },\n            };\n            await this.actionService.doAction(onCreate, options);\n        } else {\n            await this.props.createRecord();\n        }\n    }\n\n    get canCreate() {\n        const { create, createGroup } = this.props.archInfo.activeActions;\n        const list = this.model.root;\n        if (!create) {\n            return false;\n        }\n        if (list.isGrouped) {\n            if (list.groupByField.type !== \"many2one\") {\n                return true;\n            }\n            return list.groups.length || !createGroup;\n        }\n        return true;\n    }\n\n    get canQuickCreate() {\n        const { activeActions } = this.props.archInfo;\n        if (!activeActions.quickCreate) {\n            return false;\n        }\n\n        const list = this.model.root;\n        if (list.groups && !list.groups.length) {\n            return false;\n        }\n\n        return this.isQuickCreateField(list.groupByField);\n    }\n\n    onRecordSaved(record) {\n        if (this.model.root.isGrouped) {\n            const group = this.model.root.groups.find((l) =>\n                l.records.find((r) => r.id === record.id)\n            );\n            this.progressBarState?.updateCounts(group);\n        }\n    }\n\n    onPageChangeScroll() {\n        if (this.rootRef && this.rootRef.el) {\n            if (this.env.isSmall) {\n                this.rootRef.el.scrollTop = 0;\n            } else {\n                this.rootRef.el.querySelector(\".o_content\").scrollTop = 0;\n            }\n        }\n    }\n\n    async beforeExecuteActionButton(clickParams) {}\n\n    async afterExecuteActionButton(clickParams) {}\n\n    async onUpdatedPager() {}\n\n    scrollTop() {\n        this.rootRef.el.querySelector(\".o_content\").scrollTo({ top: 0 });\n    }\n\n    isQuickCreateField(field) {\n        return field && QUICK_CREATE_FIELD_TYPES.includes(field.type);\n    }\n}\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { FileInput } from \"@web/core/file_input/file_input\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { Component, useState, onWillStart } from \"@odoo/owl\";\n\nlet nextDialogId = 1;\n\nexport class KanbanCoverImageDialog extends Component {\n    static template = \"web.KanbanCoverImageDialog\";\n    static components = { Dialog, FileInput };\n    static props = [\"*\"];\n    setup() {\n        this.id = `o_cover_image_upload_${nextDialogId++}`;\n        this.orm = useService(\"orm\");\n        this.http = useService(\"http\");\n        const { record, fieldName } = this.props;\n        const attachment = (record && record.data[fieldName]) || [];\n        this.state = useState({\n            selectFile: false,\n            selectedAttachmentId: attachment[0],\n        });\n        onWillStart(async () => {\n            this.attachments = await this.orm.searchRead(\n                \"ir.attachment\",\n                [\n                    [\"res_model\", \"=\", record.resModel],\n                    [\"res_id\", \"=\", record.resId],\n                    [\"mimetype\", \"ilike\", \"image\"],\n                ],\n                [\"id\"]\n            );\n            this.state.selectFile = this.props.autoOpen && this.attachments.length;\n        });\n    }\n\n    get hasCoverImage() {\n        return Boolean(this.props.record.data[this.props.fieldName]);\n    }\n\n    onUpload([attachment]) {\n        if (!attachment) {\n            return;\n        }\n        this.state.selectFile = false;\n        this.selectAttachment(attachment, true);\n    }\n\n    selectAttachment(attachment, setSelected) {\n        if (this.state.selectedAttachmentId !== attachment.id) {\n            this.state.selectedAttachmentId = attachment.id;\n        } else {\n            this.state.selectedAttachmentId = null;\n        }\n        if (setSelected) {\n            this.setCover();\n        }\n    }\n\n    removeCover() {\n        this.state.selectedAttachmentId = null;\n        this.setCover();\n    }\n\n    async setCover() {\n        const id = this.state.selectedAttachmentId ? [this.state.selectedAttachmentId] : false;\n        await this.props.record.update({ [this.props.fieldName]: id }, { save: true });\n        this.props.close();\n    }\n\n    uploadImage() {\n        this.state.selectFile = true;\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { useDropdownCloser } from \"@web/core/dropdown/dropdown_hooks\";\n\nexport class KanbanDropdownMenuWrapper extends Component {\n    static template = \"web.KanbanDropdownMenuWrapper\";\n    static props = {\n        slots: Object,\n    };\n\n    setup() {\n        this.dropdownControl = useDropdownCloser();\n    }\n\n    onClick(ev) {\n        this.dropdownControl.closeAll();\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Component, useRef } from \"@odoo/owl\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { memoize } from \"@web/core/utils/functions\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useDebounced } from \"@web/core/utils/timing\";\nimport { isRelational } from \"@web/model/relational_model/utils\";\nimport { isNull } from \"@web/views/utils\";\nimport { ColumnProgress } from \"@web/views/view_components/column_progress\";\nimport { FormViewDialog } from \"@web/views/view_dialogs/form_view_dialog\";\nimport { registry } from \"@web/core/registry\";\nimport { utils } from \"@web/core/ui/ui_service\";\n\nclass KanbanHeaderTooltip extends Component {\n    static template = \"web.KanbanGroupTooltip\";\n    static props = {\n        tooltip: Array,\n        close: Function,\n    };\n}\n\nexport class KanbanHeader extends Component {\n    static template = \"web.KanbanHeader\";\n    static components = { ColumnProgress, Dropdown, DropdownItem };\n    static props = {\n        activeActions: { type: Object },\n        canQuickCreate: { type: Boolean },\n        deleteGroup: { type: Function },\n        dialogClose: { type: Array },\n        group: { type: Object },\n        list: { type: Object },\n        quickCreateState: { type: Object },\n        scrollTop: { type: Function },\n        tooltipInfo: { type: Object },\n        progressBarState: { type: true, optional: true },\n    };\n\n    setup() {\n        this.dialog = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n        this.rootRef = useRef(\"root\");\n        this.popover = usePopover(KanbanHeaderTooltip);\n        this.onTitleMouseEnter = useDebounced(this.onTitleMouseEnter, 400);\n    }\n\n    async onTitleMouseEnter(ev) {\n        if (!this.hasTooltip) {\n            return;\n        }\n        const tooltip = await this.loadTooltip();\n        if (tooltip.length) {\n            this.popover.open(ev.target, { tooltip });\n        }\n    }\n\n    onTitleMouseLeave() {\n        this.onTitleMouseEnter.cancel();\n        this.popover.close();\n    }\n\n    // ------------------------------------------------------------------------\n    // Getters\n    // ------------------------------------------------------------------------\n\n    get _configDropdownContainer() {\n        // FIXME: please do not override this getter in other modules.\n        // The dropdown's container prop is only used here as a workaround of\n        // a stacking context issue. It should be removed in the next release.\n        return this.rootRef.el.closest(`.o_kanban_group[data-id=\"${this.props.group.id}\"]`);\n    }\n\n    get configItems() {\n        const args = { permissions: this.permissions, props: this.props };\n        return registry\n            .category(\"kanban_header_config_items\")\n            .getEntries()\n            .map(([key, desc]) => ({\n                key,\n                method: desc.method,\n                label: desc.label,\n                isVisible:\n                    typeof desc.isVisible === \"function\" ? desc.isVisible(args) : desc.isVisible,\n                class: typeof desc.class === \"function\" ? desc.class(args) : desc.class,\n            }));\n    }\n\n    get progressBar() {\n        return this.props.progressBarState?.getGroupInfo(this.group);\n    }\n\n    get group() {\n        return this.props.group;\n    }\n\n    _getEmptyGroupLabel(fieldName) {\n        return _t(\"None\");\n    }\n\n    get groupName() {\n        const { groupByField, displayName } = this.group;\n        let name = displayName;\n        if (groupByField.type === \"boolean\") {\n            name = name ? _t(\"Yes\") : _t(\"No\");\n        } else if (!name) {\n            if (\n                isRelational(groupByField) ||\n                groupByField.type === \"date\" ||\n                groupByField.type === \"datetime\" ||\n                isNull(name)\n            ) {\n                name = this._getEmptyGroupLabel(groupByField.name);\n            }\n        }\n        return name;\n    }\n\n    get groupAggregate() {\n        const { group, progressBarState } = this.props;\n        const { sumField } = progressBarState.progressAttributes;\n        return progressBarState.getAggregateValue(group, sumField);\n    }\n\n    // ------------------------------------------------------------------------\n    // Tooltip methods\n    // ------------------------------------------------------------------------\n\n    get hasTooltip() {\n        const { name, type } = this.group.groupByField;\n        return type === \"many2one\" && this.group.value && name in this.props.tooltipInfo;\n    }\n\n    loadTooltip = memoize(async () => {\n        const { name, relation: resModel } = this.group.groupByField;\n        const tooltipInfo = this.props.tooltipInfo[name];\n        const fieldNames = Object.keys(tooltipInfo);\n        const [values] = await this.orm.silent.read(\n            resModel,\n            [this.group.value],\n            [\"display_name\", ...fieldNames]\n        );\n\n        return fieldNames\n            .filter((fieldName) => values[fieldName])\n            .map((fieldName) => ({ title: tooltipInfo[fieldName], value: values[fieldName] }));\n    });\n\n    // ------------------------------------------------------------------------\n    // Edition methods\n    // ------------------------------------------------------------------------\n\n    archiveGroup() {\n        this.dialog.add(ConfirmationDialog, {\n            body: _t(\"Are you sure that you want to archive all the records from this column?\"),\n            confirmLabel: _t(\"Archive All\"),\n            confirm: () => this.group.list.archive(),\n            cancel: () => {},\n        });\n    }\n\n    unarchiveGroup() {\n        this.group.list.unarchive();\n    }\n\n    deleteGroup() {\n        this.dialog.add(ConfirmationDialog, {\n            body: _t(\"Are you sure you want to delete this column?\"),\n            confirm: async () => {\n                this.props.deleteGroup(this.group);\n            },\n            confirmLabel: _t(\"Delete\"),\n            cancel: () => {},\n        });\n    }\n\n    editGroup() {\n        const { context, displayName, groupByField, value } = this.group;\n        this.props.dialogClose.push(\n            this.dialog.add(FormViewDialog, {\n                context,\n                resId: value,\n                resModel: groupByField.relation,\n                title: _t(\"Edit: %s\", displayName),\n                onRecordSaved: async () => {\n                    await this.props.list.load();\n                    this.props.list.model.notify();\n                },\n            })\n        );\n    }\n\n    quickCreate(group) {\n        this.props.quickCreateState.groupId = this.group.id;\n    }\n\n    toggleGroup() {\n        return this.group.toggle();\n    }\n\n    // ------------------------------------------------------------------------\n    // Permissions\n    // ------------------------------------------------------------------------\n\n    get permissions() {\n        return [\"canArchiveGroup\", \"canDeleteGroup\", \"canEditGroup\", \"canQuickCreate\"].reduce(\n            (o, key) => {\n                Object.defineProperty(o, key, { get: () => this[key]() });\n                return o;\n            },\n            {}\n        );\n    }\n\n    canArchiveGroup() {\n        const { archiveGroup } = this.props.activeActions;\n        const hasActiveField = \"active\" in this.group.fields;\n        return archiveGroup && hasActiveField && this.group.groupByField.type !== \"many2many\";\n    }\n\n    canDeleteGroup() {\n        const { deleteGroup } = this.props.activeActions;\n        const { groupByField, value } = this.group;\n        return deleteGroup && isRelational(groupByField) && value;\n    }\n\n    canEditGroup() {\n        const { editGroup } = this.props.activeActions;\n        const { groupByField, value } = this.group;\n        return editGroup && isRelational(groupByField) && value;\n    }\n\n    canQuickCreate() {\n        return this.props.canQuickCreate;\n    }\n\n    async onBarClicked(value) {\n        await this.props.progressBarState.selectBar(this.props.group.id, value);\n        this.props.scrollTop();\n    }\n}\n\nconst kanbanHeaderConfigItems = registry.category(\"kanban_header_config_items\");\nkanbanHeaderConfigItems.add(\n    \"toggle_group\",\n    {\n        label: _t(\"Fold\"),\n        method: \"toggleGroup\",\n        isVisible: () => !utils.isSmall(),\n        class: ({ props }) => ({\n            o_kanban_toggle_fold: true,\n            disabled: props.list.model.useSampleModel,\n        }),\n    },\n    { sequence: 10 }\n);\nkanbanHeaderConfigItems.add(\n    \"edit_group\",\n    {\n        label: _t(\"Edit\"),\n        method: \"editGroup\",\n        isVisible: ({ permissions }) => permissions.canEditGroup,\n        class: \"o_column_edit\",\n    },\n    { sequence: 20 }\n);\nkanbanHeaderConfigItems.add(\n    \"delete_group\",\n    {\n        label: _t(\"Delete\"),\n        method: \"deleteGroup\",\n        isVisible: ({ permissions }) => permissions.canDeleteGroup,\n        class: \"o_column_delete\",\n    },\n    { sequence: 30 }\n);\nkanbanHeaderConfigItems.add(\n    \"archive_group\",\n    {\n        label: _t(\"Archive All\"),\n        method: \"archiveGroup\",\n        isVisible: ({ permissions }) => permissions.canArchiveGroup,\n        class: ({ props }) => ({\n            o_column_archive_records: true,\n            disabled: props.list.model.useSampleModel,\n        }),\n    },\n    { sequence: 40 }\n);\nkanbanHeaderConfigItems.add(\n    \"unarchive_group\",\n    {\n        label: _t(\"Unarchive All\"),\n        method: \"unarchiveGroup\",\n        isVisible: ({ permissions }) => permissions.canArchiveGroup,\n        class: ({ props }) => ({\n            o_column_unarchive_records: true,\n            disabled: props.list.model.useSampleModel,\n        }),\n    },\n    { sequence: 50 }\n);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { ColorList } from \"@web/core/colorlist/colorlist\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { isHtmlEmpty as _isHtmlEmpty } from \"@web/core/utils/html\";\nimport { imageUrl } from \"@web/core/utils/urls\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\nimport { Field } from \"@web/views/fields/field\";\nimport { fileTypeMagicWordMap } from \"@web/views/fields/image/image_field\";\nimport { ViewButton } from \"@web/views/view_button/view_button\";\nimport { useViewCompiler } from \"@web/views/view_compiler\";\nimport { Widget } from \"@web/views/widgets/widget\";\nimport { getFormattedValue } from \"../utils\";\nimport {\n    LEGACY_KANBAN_BOX_ATTRIBUTE,\n    LEGACY_KANBAN_MENU_ATTRIBUTE,\n    KANBAN_CARD_ATTRIBUTE,\n    KANBAN_MENU_ATTRIBUTE,\n} from \"./kanban_arch_parser\";\nimport { KanbanCompiler } from \"./kanban_compiler\";\nimport { KanbanCoverImageDialog } from \"./kanban_cover_image_dialog\";\nimport { KanbanDropdownMenuWrapper } from \"./kanban_dropdown_menu_wrapper\";\n\nimport { Component, onWillUpdateProps, useRef, useState } from \"@odoo/owl\";\n\nconst { COLORS } = ColorList;\n\nconst formatters = registry.category(\"formatters\");\n\n// These classes determine whether a click on a record should open it.\nexport const CANCEL_GLOBAL_CLICK = [\"a\", \".dropdown\", \".oe_kanban_action\", \"[data-bs-toggle]\"].join(\n    \",\"\n);\n\n/**\n * Returns the index of a color determined by a given record.\n */\nexport function getColorIndex(value) {\n    if (typeof value === \"number\") {\n        return Math.round(value) % COLORS.length;\n    } else if (typeof value === \"string\") {\n        const charCodeSum = [...value].reduce((acc, _, i) => acc + value.charCodeAt(i), 0);\n        return charCodeSum % COLORS.length;\n    } else {\n        return 0;\n    }\n}\n\n/**\n * Returns a \"raw\" version of the field value on a given record.\n *\n * @param {Record} record\n * @param {string} fieldName\n * @returns {any}\n */\nexport function getRawValue(record, fieldName) {\n    const field = record.fields[fieldName];\n    const value = record.data[fieldName];\n    switch (field.type) {\n        case \"one2many\":\n        case \"many2many\": {\n            return value.count ? value.currentIds : [];\n        }\n        case \"many2one\": {\n            return (value && value[0]) || false;\n        }\n        case \"date\":\n        case \"datetime\": {\n            return value && value.toISO();\n        }\n        default: {\n            return value;\n        }\n    }\n}\n\n/**\n * Returns a formatted version of the field value on a given record.\n *\n * @param {Record} record\n * @param {string} fieldName\n * @returns {string}\n */\nfunction getValue(record, fieldName) {\n    const field = record.fields[fieldName];\n    const value = record.data[fieldName];\n    const formatter = formatters.get(field.type, String);\n    return formatter(value, { field, data: record.data });\n}\n\nexport function getFormattedRecord(record) {\n    const formattedRecord = {\n        id: {\n            value: record.resId,\n            raw_value: record.resId,\n        },\n    };\n\n    for (const fieldName of record.fieldNames) {\n        formattedRecord[fieldName] = {\n            value: getValue(record, fieldName),\n            raw_value: getRawValue(record, fieldName),\n        };\n    }\n    return formattedRecord;\n}\n\n/**\n * Returns the image URL of a given field on the record.\n *\n * @param {Record} record\n * @param {string} [model] model name\n * @param {string} [field] field name\n * @param {number | [number, ...any[]]} [idOrIds] id or array\n *      starting with the id of the desired record.\n * @param {string} [placeholder] fallback when the image does not\n *  exist\n * @returns {string}\n */\nexport function getImageSrcFromRecordInfo(record, model, field, idOrIds, placeholder) {\n    const id = (Array.isArray(idOrIds) ? idOrIds[0] : idOrIds) || null;\n    const isCurrentRecord =\n        record.resModel === model && (record.resId === id || (!record.resId && !id));\n    const fieldVal = record.data[field];\n    if (isCurrentRecord && fieldVal && !isBinSize(fieldVal)) {\n        // Use magic-word technique for detecting image type\n        const type = fileTypeMagicWordMap[fieldVal[0]];\n        return `data:image/${type};base64,${fieldVal}`;\n    } else if (placeholder && (!model || !field || !id || !fieldVal)) {\n        // Placeholder if either the model, field, id or value is missing or null.\n        return placeholder;\n    } else {\n        // Else: fetches the image related to the given id.\n        const unique = isCurrentRecord && record.data.write_date;\n        return imageUrl(model, id, field, { unique });\n    }\n}\n\nfunction isBinSize(value) {\n    return /^\\d+(\\.\\d*)? [^0-9]+$/.test(value);\n}\n\n/**\n * Checks if a html content is empty. If there are only formatting tags\n * with style attributes or a void content. Famous use case is\n * '<p style=\"...\" class=\"..\"><br></p>' added by some web editor(s).\n * Note that because the use of this method is limited, we ignore the cases\n * like there's one <img> tag in the content. In such case, even if it's the\n * actual content, we consider it empty.\n *\n * @param {string|ReturnType<import(\"@odoo/owl\").markup>} innerHTML\n * @returns {boolean} true if no content found or if containing only formatting tags\n */\nexport function isHtmlEmpty(innerHTML = \"\") {\n    return _isHtmlEmpty(innerHTML);\n}\n\nexport class KanbanRecord extends Component {\n    static components = {\n        Dropdown,\n        DropdownItem,\n        KanbanDropdownMenuWrapper,\n        Field,\n        KanbanCoverImageDialog,\n        ViewButton,\n        Widget,\n    };\n    static defaultProps = {\n        colors: COLORS,\n        deleteRecord: () => {},\n        archiveRecord: () => {},\n        openRecord: () => {},\n    };\n    static props = [\n        \"archInfo\",\n        \"canResequence?\",\n        \"colors?\",\n        \"Compiler?\",\n        \"forceGlobalClick?\",\n        \"group?\",\n        \"list\",\n        \"deleteRecord?\",\n        \"archiveRecord?\",\n        \"openRecord?\",\n        \"readonly?\",\n        \"record\",\n        \"templates\",\n        \"progressBarState?\",\n    ];\n    static Compiler = KanbanCompiler;\n    static LEGACY_KANBAN_BOX_ATTRIBUTE = LEGACY_KANBAN_BOX_ATTRIBUTE;\n    static LEGACY_KANBAN_MENU_ATTRIBUTE = LEGACY_KANBAN_MENU_ATTRIBUTE;\n    static KANBAN_CARD_ATTRIBUTE = KANBAN_CARD_ATTRIBUTE;\n    static KANBAN_MENU_ATTRIBUTE = KANBAN_MENU_ATTRIBUTE;\n    static menuTemplate = \"web.KanbanRecordMenu\";\n    static template = \"web.KanbanRecord\";\n\n    setup() {\n        this.evaluateBooleanExpr = evaluateBooleanExpr;\n        this.action = useService(\"action\");\n        this.dialog = useService(\"dialog\");\n        this.notification = useService(\"notification\");\n\n        const { archInfo, Compiler, templates } = this.props;\n        const ViewCompiler = Compiler || this.constructor.Compiler;\n        const isLegacy = archInfo.isLegacyArch;\n\n        this.templates = useViewCompiler(ViewCompiler, templates, { isLegacy });\n\n        this.menuTemplateName = this.props.archInfo.isLegacyArch\n            ? this.constructor.LEGACY_KANBAN_MENU_ATTRIBUTE\n            : this.constructor.KANBAN_MENU_ATTRIBUTE;\n        this.showMenu = this.menuTemplateName in templates;\n\n        this.dataState = useState({ record: {}, widget: {} });\n        this.createWidget(this.props);\n        onWillUpdateProps(this.createWidget);\n        useRecordObserver((record) =>\n            Object.assign(this.dataState.record, getFormattedRecord(record))\n        );\n        this.rootRef = useRef(\"root\");\n    }\n\n    get record() {\n        return this.dataState.record;\n    }\n\n    getFormattedValue(fieldId) {\n        const { archInfo, record } = this.props;\n        const { name } = archInfo.fieldNodes[fieldId];\n        return getFormattedValue(record, name, archInfo.fieldNodes[fieldId]);\n    }\n\n    /**\n     * Assigns \"widget\" properties on the kanban record.\n     *\n     * @param {Object} props\n     */\n    createWidget(props) {\n        const { archInfo, list } = props;\n        const { activeActions } = archInfo;\n        // Widget\n        const deletable =\n            activeActions.delete && (!list.groupByField || list.groupByField.type !== \"many2many\");\n        const editable = activeActions.edit;\n        this.dataState.widget = {\n            deletable,\n            editable,\n        };\n        if (archInfo.isLegacyArch) {\n            this.dataState.widget.isHtmlEmpty = isHtmlEmpty;\n        }\n    }\n\n    getRecordClasses() {\n        const { archInfo, canResequence, forceGlobalClick, record, progressBarState } = this.props;\n        const classes = [\"o_kanban_record d-flex\"];\n        if (canResequence) {\n            classes.push(\"o_draggable\");\n        }\n        if (forceGlobalClick || archInfo.openAction || archInfo.canOpenRecords) {\n            classes.push(\"cursor-pointer\");\n        }\n        if (progressBarState) {\n            const { fieldName, colors } = progressBarState.progressAttributes;\n            const value = record.data[fieldName];\n            const color = colors[value];\n            classes.push(`oe_kanban_card_${color}`);\n        }\n        if (archInfo.cardColorField) {\n            const value = record.data[archInfo.cardColorField];\n            classes.push(`o_kanban_color_${getColorIndex(value)}`);\n        }\n        if (!this.props.list.isGrouped) {\n            classes.push(\"flex-grow-1 flex-md-shrink-1 flex-shrink-0\");\n        }\n        classes.push(archInfo.cardClassName);\n        // TODO: remove when all kanban archs have been converted\n        if (archInfo.isLegacyArch) {\n            classes.push(\"o_legacy_kanban_record\");\n        }\n        return classes.join(\" \");\n    }\n\n    getMenuClasses() {\n        if (this.props.archInfo.isLegacyArch) {\n            return \"o-dropdown--legacy-kanban-record-menu\";\n        } else {\n            return \"o-dropdown--kanban-record-menu\";\n        }\n    }\n\n    /**\n     * @param {MouseEvent} ev\n     */\n    onGlobalClick(ev) {\n        if (ev.target.closest(CANCEL_GLOBAL_CLICK)) {\n            return;\n        }\n        const { archInfo, forceGlobalClick, openRecord, record } = this.props;\n        if (!forceGlobalClick && archInfo.openAction) {\n            this.action.doActionButton({\n                name: archInfo.openAction.action,\n                type: archInfo.openAction.type,\n                resModel: record.resModel,\n                resId: record.resId,\n                resIds: record.resIds,\n                context: record.context,\n                onClose: async () => {\n                    await record.model.root.load();\n                },\n            });\n        } else if (forceGlobalClick || this.props.archInfo.canOpenRecords) {\n            openRecord(record);\n        }\n    }\n\n    /**\n     * @param {Object} params\n     */\n    triggerAction(params) {\n        const { archInfo, openRecord, deleteRecord, record, archiveRecord } = this.props;\n        const { type } = params;\n        switch (type) {\n            // deprecated, records are always in edit mode in form views now, use \"open\" instead\n            case \"edit\": {\n                return openRecord(record, \"edit\");\n            }\n            case \"open\": {\n                return openRecord(record);\n            }\n            case \"archive\": {\n                return archiveRecord(record, true);\n            }\n            case \"unarchive\": {\n                return archiveRecord(record, false);\n            }\n            case \"delete\": {\n                return deleteRecord(record);\n            }\n            case \"set_cover\": {\n                const { autoOpen, fieldName } = params;\n                const widgets = Object.values(archInfo.fieldNodes)\n                    .filter((x) => x.name === fieldName)\n                    .map((x) => x.widget);\n                const field = record.fields[fieldName];\n                if (\n                    field.type === \"many2one\" &&\n                    field.relation === \"ir.attachment\" &&\n                    widgets.includes(\"attachment_image\")\n                ) {\n                    this.dialog.add(KanbanCoverImageDialog, { autoOpen, fieldName, record });\n                } else {\n                    const warning = _t(\n                        `Could not set the cover image: incorrect field (\"%s\") is provided in the view.`,\n                        fieldName\n                    );\n                    this.notification.add({ title: warning, type: \"danger\" });\n                }\n                break;\n            }\n            default: {\n                return this.notification.add(_t(\"Kanban: no action for type: %(type)s\", { type }), {\n                    type: \"danger\",\n                });\n            }\n        }\n    }\n\n    get mainTemplate() {\n        return this.props.archInfo.isLegacyArch\n            ? this.templates[this.constructor.LEGACY_KANBAN_BOX_ATTRIBUTE]\n            : this.templates[this.constructor.KANBAN_CARD_ATTRIBUTE];\n    }\n\n    /**\n     * Returns the card template's rendering context.\n     *\n     * Note: the keys answer to outdated standards but should not be altered for\n     * the sake of compatibility.\n     *\n     * @returns {Object}\n     */\n    get renderingContext() {\n        const renderingContext = {\n            context: this.props.record.context,\n            JSON,\n            luxon,\n            read_only_mode: this.props.readonly,\n            record: this.dataState.record,\n            selection_mode: this.props.forceGlobalClick,\n            widget: this.dataState.widget,\n            __comp__: Object.assign(Object.create(this), { this: this }),\n        };\n        if (this.props.archInfo.isLegacyArch) {\n            // deprecated, use <field name=\"\" widget=\"image\"/>\n            renderingContext.kanban_image = (...args) =>\n                getImageSrcFromRecordInfo(this.props.record, ...args);\n            // deprecated, use context instead\n            renderingContext.user_context = user.context;\n        }\n        return renderingContext;\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { parseXML } from \"@web/core/utils/xml\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { useOwnedDialogs, useService } from \"@web/core/utils/hooks\";\n\nimport {\n    Component,\n    onMounted,\n    onWillStart,\n    useExternalListener,\n    useRef,\n    useState,\n    useSubEnv,\n} from \"@odoo/owl\";\nimport { RPCError } from \"@web/core/network/rpc\";\nimport { extractFieldsFromArchInfo } from \"@web/model/relational_model/utils\";\nimport { formView } from \"../form/form_view\";\nimport { getDefaultConfig } from \"../view\";\nimport { FormViewDialog } from \"../view_dialogs/form_view_dialog\";\n\nconst DEFAULT_QUICK_CREATE_VIEW = {\n    // note: the required modifier is written in the format returned by the server\n    arch: /* xml */ `\n        <form>\n            <field name=\"display_name\" placeholder=\"Title\" required=\"True\" />\n        </form>`,\n};\nconst DEFAULT_QUICK_CREATE_FIELDS = {\n    display_name: { string: \"Display name\", type: \"char\" },\n};\n\nconst ACTION_SELECTORS = [\n    \".o_kanban_quick_add\",\n    \".o_kanban_load_more button\",\n    \".o-kanban-button-new\",\n];\n\nexport class KanbanQuickCreateController extends Component {\n    static props = {\n        Model: Function,\n        Renderer: Function,\n        Compiler: Function,\n        resModel: String,\n        onValidate: Function,\n        onCancel: Function,\n        fields: { type: Object },\n        context: { type: Object },\n        archInfo: { type: Object },\n    };\n    static template = \"web.KanbanQuickCreateController\";\n    setup() {\n        super.setup();\n\n        this.uiService = useService(\"ui\");\n        this.rootRef = useRef(\"root\");\n        this.state = useState({ disabled: false });\n        this.addDialog = useOwnedDialogs();\n\n        const { activeFields, fields } = extractFieldsFromArchInfo(\n            this.props.archInfo,\n            this.props.fields\n        );\n\n        const modelServices = Object.fromEntries(\n            this.props.Model.services.map((servName) => {\n                return [servName, useService(servName)];\n            })\n        );\n        modelServices.orm = useService(\"orm\");\n        const config = {\n            resModel: this.props.resModel,\n            resId: false,\n            resIds: [],\n            fields,\n            activeFields,\n            isMonoRecord: true,\n            mode: \"edit\",\n            context: this.props.context,\n        };\n        this.model = useState(new this.props.Model(this.env, { config }, modelServices));\n\n        onWillStart(() => this.model.load());\n\n        onMounted(() => {\n            this.uiActiveElement = this.uiService.activeElement;\n        });\n        // Close on outside click\n        useExternalListener(window, \"mousedown\", (/** @type {MouseEvent} */ ev) => {\n            // This target is kept in order to impeach close on outside click behavior if the click\n            // has been initiated from the quickcreate root element (mouse selection in an input...)\n            this.mousedownTarget = ev.target;\n        });\n        useExternalListener(\n            window,\n            \"click\",\n            (/** @type {MouseEvent} */ ev) => {\n                if (this.uiActiveElement !== this.uiService.activeElement) {\n                    // this component isn't in the current active element -> do nothing\n                    return;\n                }\n                const target = this.mousedownTarget || ev.target;\n                // accounts for clicking on datetime picker and legacy autocomplete\n                const gotClickedInside =\n                    target.closest(\".o_datetime_picker\") ||\n                    target.closest(\".ui-autocomplete\") ||\n                    this.rootRef.el.contains(target);\n                if (!gotClickedInside) {\n                    let force = false;\n                    for (const selector of ACTION_SELECTORS) {\n                        const closestEl = target.closest(selector);\n                        if (closestEl) {\n                            force = true;\n                            break;\n                        }\n                    }\n                    this.cancel(force);\n                }\n                this.mousedownTarget = null;\n            },\n            { capture: true }\n        );\n\n        // Key Navigation\n        useHotkey(\"enter\", () => this.validate(\"add\"), { bypassEditableProtection: true });\n        useHotkey(\"escape\", () => this.cancel(true));\n    }\n\n    async validate(mode) {\n        let resId = undefined;\n        if (this.state.disabled) {\n            return;\n        }\n        this.state.disabled = true;\n\n        const keys = Object.keys(this.model.root.activeFields);\n        if (keys.length === 1 && keys[0] === \"display_name\") {\n            const isValid = await this.model.root.checkValidity(); // needed to put the class o_field_invalid in the field\n            if (isValid) {\n                try {\n                    [resId] = await this.model.orm.call(\n                        this.props.resModel,\n                        \"name_create\",\n                        [this.model.root.data.display_name],\n                        {\n                            context: this.props.context,\n                        }\n                    );\n                } catch (e) {\n                    this.showFormDialogInError(e);\n                }\n            } else {\n                this.model.notification.add(_t(\"Display Name\"), {\n                    title: _t(\"Invalid fields: \"),\n                    type: \"danger\",\n                });\n            }\n        } else {\n            await this.model.root.save({\n                reload: false,\n                onError: (e) => this.showFormDialogInError(e),\n            });\n            resId = this.model.root.resId;\n        }\n\n        if (resId) {\n            await this.props.onValidate(resId, mode);\n            if (mode === \"add\") {\n                await this.model.load({ resId: false });\n            }\n        }\n        this.state.disabled = false;\n    }\n\n    async cancel(force) {\n        if (this.state.disabled) {\n            return;\n        }\n        if (force || !(await this.model.root.isDirty())) {\n            this.props.onCancel();\n        }\n    }\n\n    showFormDialogInError(e) {\n        // TODO: filter RPC errors more specifically (eg, for access denied, there is no point in opening a dialog)\n        if (!(e instanceof RPCError)) {\n            throw e;\n        }\n\n        const context = this.props.context;\n        const values = this.model.root.data;\n        context.default_name = values.name || values.display_name;\n        this.addDialog(FormViewDialog, {\n            resModel: this.props.resModel,\n            context,\n            title: _t(\"Create\"),\n            onRecordSaved: async (record) => {\n                await this.props.onValidate(record.resId, \"add\");\n                await this.model.load();\n            },\n        });\n    }\n\n    get className() {\n        return \"o_kanban_quick_create o_field_highlight shadow\";\n    }\n}\n\nexport class KanbanRecordQuickCreate extends Component {\n    static components = { KanbanQuickCreateController };\n    static template = \"web.KanbanRecordQuickCreate\";\n    static props = {\n        quickCreateView: { type: [String, { value: null }], optional: 1 },\n        onValidate: Function,\n        onCancel: Function,\n        group: Object,\n    };\n\n    setup() {\n        this.state = useState({\n            isLoaded: false,\n        });\n        this.viewService = useService(\"view\");\n        onMounted(() => {\n            this.getQuickCreateProps(this.props).then(() => {\n                this.state.isLoaded = true;\n            });\n        });\n        useSubEnv({\n            config: getDefaultConfig(),\n        });\n    }\n\n    async getQuickCreateProps(props) {\n        let quickCreateFields = { fields: DEFAULT_QUICK_CREATE_FIELDS };\n        let quickCreateForm = DEFAULT_QUICK_CREATE_VIEW;\n        let quickCreateRelatedModels = {};\n\n        if (props.quickCreateView) {\n            const { fields, relatedModels, views } = await this.viewService.loadViews({\n                context: { ...props.context, form_view_ref: props.quickCreateView },\n                resModel: props.group.resModel,\n                views: [[false, \"form\"]],\n            });\n            quickCreateFields = { fields: fields };\n            quickCreateForm = views.form;\n            quickCreateRelatedModels = relatedModels;\n        }\n        const models = {\n            ...quickCreateRelatedModels,\n            [props.group.resModel]: quickCreateFields,\n        };\n        const archInfo = new formView.ArchParser().parse(\n            parseXML(quickCreateForm.arch),\n            models,\n            props.group.resModel\n        );\n        this.quickCreateProps = {\n            Model: formView.Model,\n            Renderer: formView.Renderer,\n            Compiler: formView.Compiler,\n            resModel: props.group.resModel,\n            onValidate: props.onValidate,\n            onCancel: props.onCancel,\n            fields: quickCreateFields.fields,\n            context: props.group.context,\n            archInfo,\n        };\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { registry } from \"@web/core/registry\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { useSortable } from \"@web/core/utils/sortable_owl\";\nimport { isNull } from \"@web/views/utils\";\nimport { ColumnProgress } from \"@web/views/view_components/column_progress\";\nimport { useBounceButton } from \"@web/views/view_hook\";\nimport { KanbanColumnQuickCreate } from \"./kanban_column_quick_create\";\nimport { KanbanHeader } from \"./kanban_header\";\nimport { KanbanRecord } from \"./kanban_record\";\nimport { KanbanRecordQuickCreate } from \"./kanban_record_quick_create\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { Component, onWillDestroy, useRef, useState } from \"@odoo/owl\";\nimport { evaluateExpr } from \"@web/core/py_js/py\";\n\nconst DRAGGABLE_GROUP_TYPES = [\"many2one\"];\nconst MOVABLE_RECORD_TYPES = [\"char\", \"boolean\", \"integer\", \"selection\", \"many2one\"];\n\nfunction validateColumnQuickCreateExamples(data) {\n    const { allowedGroupBys = [], examples = [], foldField = \"\" } = data;\n    if (!allowedGroupBys.length) {\n        throw new Error(\"The example data must contain an array of allowed groupbys\");\n    }\n    if (!examples.length) {\n        throw new Error(\"The example data must contain an array of examples\");\n    }\n    const someHasFoldedColumns = examples.some(({ foldedColumns = [] }) => foldedColumns.length);\n    if (!foldField && someHasFoldedColumns) {\n        throw new Error(\"The example data must contain a fold field if there are folded columns\");\n    }\n}\n\nexport class KanbanRenderer extends Component {\n    static template = \"web.KanbanRenderer\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n        ColumnProgress,\n        KanbanColumnQuickCreate,\n        KanbanHeader,\n        KanbanRecord,\n        KanbanRecordQuickCreate,\n    };\n    static props = [\n        \"archInfo\",\n        \"Compiler?\", // optional in stable for backward compatibility\n        \"list\",\n        \"deleteRecord\",\n        \"openRecord\",\n        \"readonly\",\n        \"forceGlobalClick?\",\n        \"noContentHelp?\",\n        \"scrollTop?\",\n        \"canQuickCreate?\",\n        \"quickCreateState?\",\n        \"progressBarState?\",\n    ];\n\n    static defaultProps = {\n        scrollTop: () => {},\n        quickCreateState: { groupId: false },\n        tooltipInfo: {},\n    };\n\n    setup() {\n        this.dialogClose = [];\n        /**\n         * @type {{ processedIds: string[], columnQuickCreateIsFolded: boolean }}\n         */\n        this.state = useState({\n            processedIds: [],\n            columnQuickCreateIsFolded:\n                !this.props.list.isGrouped || this.props.list.groups.length > 0,\n        });\n        this.dialog = useService(\"dialog\");\n        this.exampleData = registry\n            .category(\"kanban_examples\")\n            .get(this.props.archInfo.examples, null);\n        if (this.exampleData) {\n            validateColumnQuickCreateExamples(this.exampleData);\n        }\n        this.ghostColumns = this.generateGhostColumns();\n\n        // Sortable\n        let dataRecordId;\n        let dataGroupId;\n        this.rootRef = useRef(\"root\");\n        if (this.canUseSortable) {\n            useSortable({\n                enable: () => this.canResequenceRecords,\n                // Params\n                ref: this.rootRef,\n                elements: \".o_draggable\",\n                ignore: \".dropdown,select\",\n                groups: () => this.props.list.isGrouped && \".o_kanban_group\",\n                connectGroups: () => this.canMoveRecords,\n                cursor: \"move\",\n                // Hooks\n                onDragStart: (params) => {\n                    const { element, group } = params;\n                    dataRecordId = element.dataset.id;\n                    dataGroupId = group && group.dataset.id;\n                    return this.sortStart(params);\n                },\n                onDragEnd: (params) => this.sortStop(params),\n                onGroupEnter: (params) => this.sortRecordGroupEnter(params),\n                onGroupLeave: (params) => this.sortRecordGroupLeave(params),\n                onDrop: (params) => this.sortRecordDrop(dataRecordId, dataGroupId, params),\n            });\n            useSortable({\n                enable: () => this.canResequenceGroups,\n                // Params\n                ref: this.rootRef,\n                elements: \".o_group_draggable\",\n                handle: \".o_column_title\",\n                cursor: \"move\",\n                // Hooks\n                onDragStart: (params) => {\n                    const { element } = params;\n                    dataGroupId = element.dataset.id;\n                    return this.sortStart(params);\n                },\n                onDragEnd: (params) => this.sortStop(params),\n                onDrop: (params) => this.sortGroupDrop(dataGroupId, params),\n            });\n        }\n\n        useBounceButton(this.rootRef, (clickedEl) => {\n            if (\n                this.props.list.isGrouped\n                    ? !this.props.list.recordCount\n                    : !this.props.list.count || this.props.list.model.useSampleModel\n            ) {\n                return clickedEl.matches(\n                    [\n                        \".o_kanban_renderer\",\n                        \".o_kanban_group\",\n                        \".o_kanban_header\",\n                        \".o_column_quick_create\",\n                        \".o_view_nocontent_smiling_face\",\n                    ].join(\", \")\n                );\n            }\n            return false;\n        });\n        onWillDestroy(() => {\n            this.dialogClose.forEach((close) => close());\n        });\n\n        if (this.env.searchModel) {\n            useBus(this.env.searchModel, \"focus-view\", () => {\n                const { model } = this.props.list;\n                if (model.useSampleModel || !model.hasData()) {\n                    return;\n                }\n                const firstCard = this.rootRef.el.querySelector(\".o_kanban_record\");\n                if (firstCard) {\n                    // Focus first kanban card\n                    firstCard.focus();\n                }\n            });\n        }\n\n        useHotkey(\n            \"Enter\",\n            ({ target }) => {\n                if (!target.classList.contains(\"o_kanban_record\")) {\n                    return;\n                }\n\n                if (this.props.archInfo.canOpenRecords) {\n                    target.click();\n                    return;\n                }\n\n                // Open first link\n                const firstLink = target.querySelector(\"a, button\");\n                if (firstLink) {\n                    firstLink.click();\n                }\n            },\n            { area: () => this.rootRef.el }\n        );\n\n        const arrowsOptions = { area: () => this.rootRef.el, allowRepeat: true };\n        if (this.env.searchModel) {\n            useHotkey(\n                \"ArrowUp\",\n                ({ area }) => {\n                    if (!this.focusNextCard(area, \"up\")) {\n                        this.env.searchModel.trigger(\"focus-search\");\n                    }\n                },\n                arrowsOptions\n            );\n        }\n        useHotkey(\"ArrowDown\", ({ area }) => this.focusNextCard(area, \"down\"), arrowsOptions);\n        useHotkey(\"ArrowLeft\", ({ area }) => this.focusNextCard(area, \"left\"), arrowsOptions);\n        useHotkey(\"ArrowRight\", ({ area }) => this.focusNextCard(area, \"right\"), arrowsOptions);\n    }\n\n    // ------------------------------------------------------------------------\n    // Getters\n    // ------------------------------------------------------------------------\n\n    get canUseSortable() {\n        return !this.env.isSmall;\n    }\n\n    get canMoveRecords() {\n        if (!this.canResequenceRecords) {\n            return false;\n        }\n        const groupByField = this.props.list.groupByField;\n        if (!groupByField) {\n            return true;\n        }\n        const fieldNodes = Object.values(this.props.archInfo.fieldNodes).filter(\n            (fieldNode) => fieldNode.name === groupByField.name\n        );\n        let isReadonly = this.props.list.fields[groupByField.name].readonly;\n        if (!isReadonly && fieldNodes.length) {\n            isReadonly = fieldNodes.every((fieldNode) => {\n                if (!fieldNode.readonly) {\n                    return false;\n                }\n                try {\n                    return evaluateExpr(fieldNode.readonly, this.props.list.evalContext);\n                } catch {\n                    return false;\n                }\n            });\n        }\n        return !isReadonly && this.isMovableField(groupByField);\n    }\n\n    get canResequenceGroups() {\n        if (!this.props.list.isGrouped) {\n            return false;\n        }\n        const { type } = this.props.list.groupByField;\n        const { groupsDraggable } = this.props.archInfo;\n        return groupsDraggable && DRAGGABLE_GROUP_TYPES.includes(type);\n    }\n\n    get canResequenceRecords() {\n        const { isGrouped, orderBy } = this.props.list;\n        const { handleField, recordsDraggable } = this.props.archInfo;\n        return Boolean(\n            recordsDraggable &&\n                (isGrouped || (handleField && (!orderBy[0] || orderBy[0].name === handleField)))\n        );\n    }\n\n    get showNoContentHelper() {\n        const { model, isGrouped, groupByField, groups } = this.props.list;\n        if (model.useSampleModel) {\n            return true;\n        }\n        if (isGrouped) {\n            if (this.props.quickCreateState.groupId) {\n                return false;\n            }\n            if (this.canCreateGroup() && !this.state.columnQuickCreateIsFolded) {\n                return false;\n            }\n            if (groups.length === 0) {\n                return groupByField.type !== \"many2one\";\n            }\n        }\n        return !model.hasData();\n    }\n\n    /**\n     * When the kanban records are grouped, the 'false' or 'undefined' group\n     * must appear first.\n     * @returns {any[]}\n     */\n    getGroupsOrRecords() {\n        const { list } = this.props;\n        if (list.isGrouped) {\n            return [...list.groups]\n                .sort((a, b) => (a.value && !b.value ? 1 : !a.value && b.value ? -1 : 0))\n                .map((group, i) => ({\n                    group,\n                    key: isNull(group.value) ? `group_key_${i}` : String(group.value),\n                }));\n        } else {\n            return list.records.map((record) => ({ record, key: record.id }));\n        }\n    }\n\n    /**\n     * @param {RelationalGroup} group\n     * @param {boolean} isGroupProcessing\n     * @returns {string}\n     */\n    getGroupClasses(group, isGroupProcessing) {\n        const classes = [];\n        if (!isGroupProcessing && this.canResequenceGroups && group.value) {\n            classes.push(\"o_group_draggable\");\n        }\n        if (!group.count) {\n            classes.push(\"o_kanban_no_records\");\n        }\n        if (!this.env.isSmall && group.isFolded) {\n            classes.push(\"o_column_folded\", \"flex-basis-0\");\n        }\n        if (this.props.progressBarState && !group.isFolded) {\n            const progressBarInfo = this.props.progressBarState.getGroupInfo(group);\n            if (progressBarInfo.activeBar) {\n                const progressBar = progressBarInfo.bars.find(\n                    (b) => b.value === progressBarInfo.activeBar\n                );\n                classes.push(\"o_kanban_group_show\", `o_kanban_group_show_${progressBar.color}`);\n            }\n        }\n        return classes.join(\" \");\n    }\n\n    getGroupUnloadedCount(group) {\n        const records = group.list.records.filter((r) => !r.isInQuickCreation);\n        const count = this.props.progressBarState?.getGroupCount(group) || group.count;\n        return count - records.length;\n    }\n\n    generateGhostColumns() {\n        let colNames;\n        if (this.exampleData && this.exampleData.ghostColumns) {\n            colNames = this.exampleData.ghostColumns;\n        } else {\n            colNames = [1, 2, 3, 4].map((num) => _t(\"Column %s\", num));\n        }\n        return colNames.map((colName) => ({\n            name: colName,\n            cards: new Array(Math.floor(Math.random() * 4) + 2),\n        }));\n    }\n\n    /**\n     * @param {string} id\n     * @returns {boolean}\n     */\n    isProcessing(id) {\n        return this.state.processedIds.includes(id);\n    }\n\n    isMovableField(field) {\n        return MOVABLE_RECORD_TYPES.includes(field.type);\n    }\n\n    // ------------------------------------------------------------------------\n    // Permissions\n    // ------------------------------------------------------------------------\n\n    canCreateGroup() {\n        const { activeActions } = this.props.archInfo;\n        return activeActions.createGroup && this.props.list.groupByField.type === \"many2one\";\n    }\n\n    canQuickCreate() {\n        return this.props.canQuickCreate;\n    }\n\n    // ------------------------------------------------------------------------\n    // Edition methods\n    // ------------------------------------------------------------------------\n\n    async archiveRecord(record, active) {\n        if (active) {\n            this.dialog.add(ConfirmationDialog, {\n                body: _t(\"Are you sure that you want to archive this record?\"),\n                confirmLabel: _t(\"Archive\"),\n                confirm: () => record.archive(),\n                cancel: () => {},\n            });\n        } else {\n            return record.unarchive();\n        }\n    }\n\n    async validateQuickCreate(recordId, mode, group) {\n        this.props.quickCreateState.groupId = false;\n        if (mode === \"add\") {\n            this.props.quickCreateState.groupId = group.id;\n        }\n        const record = await group.addExistingRecord(recordId, true);\n        group.model.bus.trigger(\"group-updated\", {\n            group: group,\n            withProgressBars: true,\n        });\n        if (mode === \"edit\") {\n            await this.props.openRecord(record, \"edit\");\n        } else {\n            this.props.progressBarState?.updateCounts(group);\n        }\n    }\n\n    cancelQuickCreate() {\n        this.props.quickCreateState.groupId = false;\n    }\n\n    async deleteGroup(group) {\n        await this.props.list.deleteGroups([group]);\n        if (this.props.list.groups.length === 0) {\n            this.state.columnQuickCreateIsFolded = false;\n        }\n    }\n\n    toggleGroup(group) {\n        return group.toggle();\n    }\n\n    loadMore(group) {\n        return group.list.load({ limit: group.list.records.length + group.model.initialLimit });\n    }\n\n    /**\n     * @param {string} id\n     * @param {boolean} isProcessing\n     */\n    toggleProcessing(id, isProcessing) {\n        if (isProcessing) {\n            this.state.processedIds = [...this.state.processedIds, id];\n        } else {\n            this.state.processedIds = this.state.processedIds.filter(\n                (processedId) => processedId !== id\n            );\n        }\n    }\n\n    // ------------------------------------------------------------------------\n    // Handlers\n    // ------------------------------------------------------------------------\n\n    async onGroupClick(group, ev) {\n        if (!this.env.isSmall && group.isFolded) {\n            await group.toggle();\n            this.props.scrollTop();\n        }\n    }\n\n    /**\n     * @param {string} dataGroupId\n     * @param {Object} params\n     * @param {HTMLElement} params.element\n     * @param {HTMLElement} [params.group]\n     * @param {HTMLElement} [params.next]\n     * @param {HTMLElement} [params.parent]\n     * @param {HTMLElement} [params.previous]\n     */\n    async sortGroupDrop(dataGroupId, { previous }) {\n        this.toggleProcessing(dataGroupId, true);\n        const refId = previous ? previous.dataset.id : null;\n        try {\n            await this.props.list.resequence(dataGroupId, refId);\n        } finally {\n            this.toggleProcessing(dataGroupId, false);\n        }\n    }\n\n    /**\n     * @param {string} dataRecordId\n     * @param {string} dataGroupId\n     * @param {Object} params\n     * @param {HTMLElement} params.element\n     * @param {HTMLElement} [params.group]\n     * @param {HTMLElement} [params.next]\n     * @param {HTMLElement} [params.parent]\n     * @param {HTMLElement} [params.previous]\n     */\n    async sortRecordDrop(dataRecordId, dataGroupId, { element, parent, previous }) {\n        if (\n            !this.props.list.isGrouped ||\n            parent.classList.contains(\"o_kanban_hover\") ||\n            parent.dataset.id === element.parentElement.dataset.id\n        ) {\n            this.toggleProcessing(dataRecordId, true);\n\n            parent?.classList.remove(\"o_kanban_hover\");\n            while (previous && !previous.dataset.id) {\n                previous = previous.previousElementSibling;\n            }\n            const refId = previous ? previous.dataset.id : null;\n            const targetGroupId = parent?.dataset.id;\n            try {\n                await this.props.list.moveRecord(dataRecordId, dataGroupId, refId, targetGroupId);\n            } finally {\n                this.toggleProcessing(dataRecordId, false);\n            }\n        }\n    }\n\n    /**\n     * @param {Object} params\n     * @param {HTMLElement} params.group\n     */\n    sortRecordGroupEnter({ group }) {\n        group.classList.add(\"o_kanban_hover\");\n    }\n\n    /**\n     * @param {Object} params\n     * @param {HTMLElement} params.group\n     */\n    sortRecordGroupLeave({ group }) {\n        group.classList.remove(\"o_kanban_hover\");\n    }\n\n    /**\n     * @param {Object} params\n     * @param {HTMLElement} params.element\n     * @param {HTMLElement} [params.group]\n     */\n    sortStart({ element }) {\n        element.classList.add(\"shadow\");\n    }\n\n    /**\n     * @param {Object} params\n     * @param {HTMLElement} params.element\n     * @param {HTMLElement} [params.group]\n     */\n    sortStop({ element, group }) {\n        element.classList.remove(\"shadow\");\n        if (group) {\n            group.classList.remove(\"o_kanban_hover\");\n        }\n    }\n\n    /**\n     * Focus next card in the area within the chosen direction.\n     *\n     * @param {HTMLElement} area\n     * @param {\"down\"|\"up\"|\"right\"|\"left\"} direction\n     * @returns {true?} true if the next card has been focused\n     */\n    focusNextCard(area, direction) {\n        const { isGrouped } = this.props.list;\n        const closestCard = document.activeElement.closest(\".o_kanban_record\");\n        if (!closestCard) {\n            return;\n        }\n        const groups = isGrouped ? [...area.querySelectorAll(\".o_kanban_group\")] : [area];\n        const cards = [...groups]\n            .map((group) => [...group.querySelectorAll(\".o_kanban_record\")])\n            .filter((group) => group.length);\n\n        let iGroup;\n        let iCard;\n        for (iGroup = 0; iGroup < cards.length; iGroup++) {\n            const i = cards[iGroup].indexOf(closestCard);\n            if (i !== -1) {\n                iCard = i;\n                break;\n            }\n        }\n        // Find next card to focus\n        let nextCard;\n        switch (direction) {\n            case \"down\":\n                nextCard = iCard < cards[iGroup].length - 1 && cards[iGroup][iCard + 1];\n                break;\n            case \"up\":\n                nextCard = iCard > 0 && cards[iGroup][iCard - 1];\n                break;\n            case \"right\":\n                if (isGrouped) {\n                    nextCard = iGroup < cards.length - 1 && cards[iGroup + 1][0];\n                } else {\n                    nextCard = iCard < cards[0].length - 1 && cards[0][iCard + 1];\n                }\n                break;\n            case \"left\":\n                if (isGrouped) {\n                    nextCard = iGroup > 0 && cards[iGroup - 1][0];\n                } else {\n                    nextCard = iCard > 0 && cards[0][iCard - 1];\n                }\n                break;\n        }\n\n        if (nextCard && nextCard instanceof HTMLElement) {\n            nextCard.focus();\n            return true;\n        }\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { RelationalModel } from \"@web/model/relational_model/relational_model\";\nimport { KanbanArchParser } from \"./kanban_arch_parser\";\nimport { KanbanCompiler } from \"./kanban_compiler\";\nimport { KanbanController } from \"./kanban_controller\";\nimport { KanbanRenderer } from \"./kanban_renderer\";\n\nexport const kanbanView = {\n    type: \"kanban\",\n\n    ArchParser: KanbanArchParser,\n    Controller: KanbanController,\n    Model: RelationalModel,\n    Renderer: KanbanRenderer,\n    Compiler: KanbanCompiler,\n\n    buttonTemplate: \"web.KanbanView.Buttons\",\n\n    props: (genericProps, view) => {\n        const { arch, relatedModels, resModel } = genericProps;\n        const { ArchParser } = view;\n        const archInfo = new ArchParser().parse(arch, relatedModels, resModel);\n        const defaultGroupBy =\n            genericProps.searchMenuTypes.includes(\"groupBy\") && archInfo.defaultGroupBy;\n\n        return {\n            ...genericProps,\n            // Compiler: view.Compiler, // don't pass it automatically in stable, for backward compat\n            Model: view.Model,\n            Renderer: view.Renderer,\n            buttonTemplate: view.buttonTemplate,\n            archInfo,\n            defaultGroupBy,\n        };\n    },\n};\n\nregistry.category(\"views\").add(\"kanban\", kanbanView);\n", "import { reactive } from \"@odoo/owl\";\nimport { Domain } from \"@web/core/domain\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { extractInfoFromGroupData } from \"@web/model/relational_model/utils\";\n\nconst FALSE = Symbol(\"False\");\n\n/**\n *\n * @param {*} groups: returned by web_read_group\n * @param {*} groupByField\n * @param {*} value\n * @returns\n */\n\nfunction _findGroup(groups, groupByField, value) {\n    return groups.find((g) => g[groupByField.name] === value) || {};\n}\n\nfunction _createFilterDomain(fieldName, bars, value) {\n    let filterDomain = undefined;\n    if (value === FALSE) {\n        const keys = bars.filter((x) => x.value !== FALSE).map((x) => x.value);\n        filterDomain = [\"!\", [fieldName, \"in\", keys]];\n    } else {\n        filterDomain = [[fieldName, \"=\", value]];\n    }\n    return filterDomain;\n}\n\nfunction _groupsToAggregateValues(groups, groupBy, fields) {\n    const groupByFieldName = groupBy[0].split(\":\")[0];\n    return groups.map((g) => {\n        const groupInfo = extractInfoFromGroupData(g, groupBy, fields);\n        return Object.assign(groupInfo.aggregates, { [groupByFieldName]: groupInfo.serverValue });\n    });\n}\n\nclass ProgressBarState {\n    constructor(progressAttributes, model, aggregateFields, activeBars = {}) {\n        this.progressAttributes = progressAttributes;\n        this.model = model;\n        this._groupsInfo = {};\n        this._aggregateFields = aggregateFields;\n        this.activeBars = activeBars;\n        this._aggregateValues = [];\n        this._pbCounts = null;\n    }\n\n    getGroupInfo(group) {\n        if (!this._groupsInfo[group.id]) {\n            const aggValues = _findGroup(\n                this._aggregateValues,\n                group.groupByField,\n                group.serverValue\n            );\n            const index = this._aggregateValues.indexOf(aggValues);\n            if (index > -1) {\n                this._aggregateValues.splice(index, 1);\n            }\n            this._aggregateValues.push({\n                ...group.aggregates,\n                [group.groupByField.name]: group.serverValue,\n            });\n            const groupValue = this._getGroupValue(group);\n            const pbCount = this._pbCounts[groupValue];\n            const { fieldName, colors } = this.progressAttributes;\n            const { selection: fieldSelection } = this.model.root.fields[fieldName];\n            const selection = fieldSelection && Object.fromEntries(fieldSelection);\n            const bars = Object.entries(colors).map(([value, color]) => {\n                let string;\n                if (selection) {\n                    string = selection[value];\n                } else {\n                    string = String(value);\n                }\n                return {\n                    count: (pbCount && pbCount[value]) || 0,\n                    value,\n                    string,\n                    color,\n                };\n            });\n            bars.push({\n                count: group.count - bars.map((r) => r.count).reduce((a, b) => a + b, 0),\n                value: FALSE,\n                string: _t(\"Other\"),\n                color: \"200\",\n            });\n\n            // Update activeBars count and aggreagates\n            if (this.activeBars[group.serverValue]) {\n                this.activeBars[group.serverValue].count = bars.find(\n                    (x) => x.value === this.activeBars[group.serverValue].value\n                ).count;\n\n                if (this.activeBars[group.serverValue].count === 0) {\n                    group.applyFilter(undefined).then(() => {\n                        delete this.activeBars[group.serverValue];\n                        group.model.notify();\n                    });\n                }\n\n                if (this._aggregateFields.length) {\n                    //recompute the aggregates is not necessary\n                    //the web_read_group was already done with the correct domain (containing the applied filter)\n                    this.activeBars[group.serverValue].aggregates = _findGroup(\n                        this._aggregateValues,\n                        group.groupByField,\n                        group.serverValue\n                    );\n                }\n            }\n\n            const self = this;\n            const progressBar = {\n                get activeBar() {\n                    return self.activeBars[group.serverValue]?.value || null;\n                },\n                bars,\n            };\n\n            this._groupsInfo[group.id] = progressBar;\n        }\n        return this._groupsInfo[group.id];\n    }\n\n    getAggregateValue(group, aggregateField) {\n        const title = aggregateField ? aggregateField.string : _t(\"Count\");\n        let value = 0;\n        if (!this.activeBars[group.serverValue]) {\n            value = group.count;\n            if (aggregateField) {\n                value =\n                    _findGroup(this._aggregateValues, group.groupByField, group.serverValue)[\n                        aggregateField.name\n                    ] || 0;\n            }\n        } else {\n            value = this.activeBars[group.serverValue].count;\n            if (aggregateField) {\n                value =\n                    (this.activeBars[group.serverValue]?.aggregates &&\n                        this.activeBars[group.serverValue]?.aggregates[aggregateField.name]) ||\n                    0;\n            }\n        }\n        return { title, value };\n    }\n\n    async selectBar(groupId, bar) {\n        const group = this.model.root.groups.find((group) => group.id === groupId);\n        const progressBar = this.getGroupInfo(group);\n        const nextActiveBar = {};\n        if (bar.value && this.activeBars[group.serverValue]?.value !== bar.value) {\n            nextActiveBar.value = bar.value;\n        } else {\n            group.applyFilter(undefined).then(() => {\n                delete this.activeBars[group.serverValue];\n                group.model.notify();\n            });\n            return;\n        }\n        const { bars } = progressBar;\n        const filterDomain = _createFilterDomain(\n            this.progressAttributes.fieldName,\n            bars,\n            nextActiveBar.value\n        );\n        const proms = [];\n        proms.push(\n            group.applyFilter(filterDomain).then((res) => {\n                const groupInfo = this.getGroupInfo(group);\n                nextActiveBar.count = groupInfo.bars.find(\n                    (x) => x.value === nextActiveBar.value\n                ).count;\n            })\n        );\n        if (this._aggregateFields.length) {\n            proms.push(this._updateAggregateGroup(group, bars, nextActiveBar));\n        }\n        await Promise.all(proms);\n        this.activeBars[group.serverValue] = nextActiveBar;\n        this.updateCounts(group);\n    }\n\n    _updateAggregateGroup(group, bars, activeBar) {\n        const filterDomain = _createFilterDomain(\n            this.progressAttributes.fieldName,\n            bars,\n            activeBar.value\n        );\n        const { context, fields, groupBy, resModel } = this.model.root;\n        const kwargs = { context };\n        const fieldNames = [...this._aggregateFields.map((f) => f.name), group.groupByField.name];\n        const domain = filterDomain\n            ? Domain.and([group.groupDomain, filterDomain]).toList()\n            : group.groupDomain;\n        return this.model.orm\n            .webReadGroup(resModel, domain, fieldNames, groupBy, kwargs)\n            .then((res) => {\n                if (res.length) {\n                    const groupByField = group.groupByField;\n                    const aggrValues = _groupsToAggregateValues(res.groups, groupBy, fields);\n                    activeBar.aggregates = _findGroup(aggrValues, groupByField, group.serverValue);\n                }\n            });\n    }\n\n    updateCounts(group) {\n        this._updateProgressBar();\n        if (this._aggregateFields.length) {\n            this._updateAggregates();\n            this.updateAggreagteGroup(group);\n        }\n\n        // If the selected bar is empty, remove the selection\n        for (const group of this.model.root.groups) {\n            if (this.activeBars[group.serverValue] && group.list.count === 0) {\n                this.selectBar(group.id, { value: null });\n            }\n        }\n    }\n\n    updateAggreagteGroup(group) {\n        if (group && this.activeBars[group.serverValue]) {\n            const { bars } = this.getGroupInfo(group);\n            this._updateAggregateGroup(group, bars, this.activeBars[group.serverValue]);\n        }\n    }\n\n    async _updateAggregates() {\n        const { context, fields, groupBy, domain, resModel } = this.model.root;\n        const fieldsName = this._aggregateFields.map((f) => f.name);\n        const firstGroupByName = groupBy[0].split(\":\")[0];\n        const kwargs = { context };\n        const res = await this.model.orm.webReadGroup(\n            resModel,\n            domain,\n            [...fieldsName, firstGroupByName],\n            groupBy,\n            kwargs\n        );\n        this._aggregateValues = _groupsToAggregateValues(res.groups, groupBy, fields);\n    }\n\n    async _updateProgressBar() {\n        const groupBy = this.model.root.groupBy;\n        const defaultGroupBy = this.model.root.defaultGroupBy;\n        if (groupBy.length || defaultGroupBy) {\n            const resModel = this.model.root.resModel;\n            const domain = this.model.root.domain;\n            const context = this.model.root.context;\n            const { colors, fieldName: field, help } = this.progressAttributes;\n            const groupsId = this.model.root.groups.map((g) => g.id).join();\n            const res = await this.model.orm.call(resModel, \"read_progress_bar\", [], {\n                domain,\n                group_by: groupBy.length ? groupBy[0] : defaultGroupBy,\n                progress_bar: { colors, field, help },\n                context,\n            });\n            if (groupsId !== this.model.root.groups.map((g) => g.id).join()) {\n                return;\n            }\n            this._pbCounts = res;\n            for (const group of this.model.root.groups) {\n                if (!group.isFolded) {\n                    const groupInfo = this.getGroupInfo(group);\n                    const groupValue = this._getGroupValue(group);\n                    const counts = res[groupValue];\n                    for (const bar of groupInfo.bars) {\n                        bar.count = (counts && counts[bar.value]) || 0;\n                    }\n                    groupInfo.bars.find((b) => b.value === FALSE).count = counts\n                        ? group.count - Object.values(counts).reduce((a, b) => a + b, 0)\n                        : group.count;\n\n                    if (this.activeBars[group.serverValue]) {\n                        this.activeBars[group.serverValue].count = groupInfo.bars.find(\n                            (x) => x.value === this.activeBars[group.serverValue].value\n                        ).count;\n                    }\n                }\n            }\n        }\n    }\n\n    async loadProgressBar({ context, domain, groupBy, resModel }) {\n        if (groupBy.length) {\n            const { colors, fieldName: field, help } = this.progressAttributes;\n            const res = await this.model.orm.call(resModel, \"read_progress_bar\", [], {\n                domain,\n                group_by: groupBy[0],\n                progress_bar: { colors, field, help },\n                context,\n            });\n            this._pbCounts = res;\n        }\n    }\n\n    getGroupCount(group) {\n        const progressBarInfo = this.getGroupInfo(group);\n        if (progressBarInfo.activeBar) {\n            const progressBar = progressBarInfo.bars.find(\n                (b) => b.value === progressBarInfo.activeBar\n            );\n            return progressBar.count;\n        }\n    }\n\n    /**\n     * We must be able to match groups returned by the read_progress_bar call with groups previously\n     * returned by web_read_group. When grouped on date(time) fields, the key of each group is the\n     * displayName of the period (e.g. \"W8 2024\"). When grouped on boolean fields, it's \"True\" and\n     * \"False\". For falsy values (e.g. unset many2one), it's \"False\". In all other cases, it's the\n     * group's value (e.g. the id for a many2one).\n     *\n     * @param {Group} group\n     * @return string\n     */\n    _getGroupValue(group) {\n        if (group.groupByField.type === \"date\" || group.groupByField.type === \"datetime\") {\n            return group.displayName || \"False\";\n        }\n        if (group.value === true) {\n            return \"True\";\n        } else if (group.value === false) {\n            return \"False\";\n        }\n        return group.value;\n    }\n}\n\nexport function useProgressBar(progressAttributes, model, aggregateFields, activeBars) {\n    const progressBarState = reactive(\n        new ProgressBarState(progressAttributes, model, aggregateFields, activeBars)\n    );\n\n    let prom;\n    const onWillLoadRoot = model.hooks.onWillLoadRoot;\n    model.hooks.onWillLoadRoot = (config) => {\n        onWillLoadRoot();\n        prom = progressBarState.loadProgressBar({\n            context: config.context,\n            domain: config.domain,\n            groupBy: config.groupBy,\n            resModel: config.resModel,\n        });\n    };\n    const onRootLoaded = model.hooks.onRootLoaded;\n    model.hooks.onRootLoaded = async () => {\n        await onRootLoaded();\n        return prom;\n    };\n\n    return progressBarState;\n}\n", "import { useDebounced } from \"@web/core/utils/timing\";\n\nimport { useComponent, useEffect, useExternalListener } from \"@odoo/owl\";\n\n// This file defines a hook that encapsulates the column width logic of the list view. This logic\n// aims at optimizing the available space between columns and, once computed, at freezing the table\n// to ensure that the columns don't flicker. This hook is meant to be used by the ListRenderer only,\n// it isn't a generic hook that can be used in various contexts.\n//\n// Widths computation specs\n// ------------------------\n//\n// For some field types, we harcode the column width because we know the required space to display\n// values for that type (e.g. a Date field always requires the same space). A width can also be\n// hardcoded in the arch (`width=\"60px\"`). In those cases, the column has a fixed width that we\n// enforce. Note that the column width will be the given width + the cell's left and right paddings.\n// Numeric fields don't technically have a fixed width, but rather a range: we always want enough\n// space s.t. `1 million` would fit, and we consider that we don't need more space than `1 billion`\n// would require to fit. Depending on the field type (integer, float, monetary), we determine the\n// necessary width to display those numbers.\n// The other columns have an hardcoded min width, that we always want to guarantee, but they have no\n// max width.\n//\n// There're two cases. In both of them, we need to compute a starting point for the widths:\n//   - there's no data in the table: we force all columns with hardcoded widths to those widths and\n//     uniformly distribute the remaining space among the other columns.\n//   - there're records in the table, we let the browser compute ideal widths based on the content\n//     of the table.\n// Once this is done, we ensure that each column complies with their min and max widths. It may\n// happen that some columns are too narrow (because their content is small, and there're a lot of\n// columns), so we expand them to their minimal width. It may also happen that some columns are too\n// wide (if they have a max width), so we shrink them.\n// Once this is done, we must ensure that the sum of the column widths still fills 100% of the\n// table. That means that we might have to expand/narrow columns, again. It may happen that the\n// table has too many columns s.t. they can't fit within the 100% by complying the the rules, it's\n// fine, an horizontal scrollbar will be displayed in that case.\n//\n// Freeze logic\n// ------------\n//\n// Once optimal widths have been computed, we want the table to be frozen s.t. columns don't resize\n// upon user interaction, like inline edition, adding or removing a record... The computed widths\n// are thus stored, and re-applied at each rendering. There're exceptions though. If the columns\n// change (e.g. optional column toggled), if the window is resized, if we remove a filter or open\n// a group s.t. the list contains records for the first time, we forget the computed widths and\n// start over.\n\n// Hardcoded widths\nconst DEFAULT_MIN_WIDTH = 80;\nconst SELECTOR_WIDTH = 20;\nconst OPEN_FORM_VIEW_BUTTON_WIDTH = 54;\nconst DELETE_BUTTON_WIDTH = 12;\nconst FIELD_WIDTHS = {\n    boolean: [20, 100], // [minWidth, maxWidth]\n    char: [80], // only minWidth, no maxWidth\n    date: 80, // minWidth = maxWidth\n    datetime: 145,\n    float: 93,\n    integer: 71,\n    many2many: [80],\n    many2one_reference: [80],\n    many2one: [80],\n    monetary: 105,\n    one2many: [80],\n    reference: [80],\n    selection: [80],\n    text: [80, 1200],\n};\n\n/**\n * Compute ideal widths based on the rules described on top of this file.\n *\n * @params {Element} table\n * @params {Object} state\n * @params {Number} allowedWidth\n * @params {Number[]} startingWidths\n * @returns {Number[]}\n */\nfunction computeWidths(table, state, allowedWidth, startingWidths) {\n    let _columnWidths;\n    const headers = [...table.querySelectorAll(\"thead th\")];\n    const columns = state.columns;\n\n    // Starting point: compute widths\n    if (startingWidths) {\n        _columnWidths = startingWidths.slice();\n    } else if (state.isEmpty) {\n        // Table is empty => uniform distribution as starting point\n        _columnWidths = headers.map(() => allowedWidth / headers.length);\n    } else {\n        // Table contains records => let the browser compute ideal widths\n        // Set table layout auto and remove inline style\n        table.style.tableLayout = \"auto\";\n        headers.forEach((th) => {\n            th.style.width = null;\n        });\n        // Toggle a className used to remove style that could interfere with the ideal width\n        // computation algorithm (e.g. prevent text fields from being wrapped during the\n        // computation, to prevent them from being completely crushed)\n        table.classList.add(\"o_list_computing_widths\");\n        _columnWidths = headers.map((th) => th.getBoundingClientRect().width);\n        table.classList.remove(\"o_list_computing_widths\");\n    }\n\n    // Force columns to comply with their min and max widths\n    if (state.hasSelectors) {\n        _columnWidths[0] = SELECTOR_WIDTH;\n    }\n    if (state.hasOpenFormViewColumn) {\n        const index = _columnWidths.length - (state.hasActionsColumn ? 2 : 1);\n        _columnWidths[index] = OPEN_FORM_VIEW_BUTTON_WIDTH;\n    }\n    if (state.hasActionsColumn) {\n        _columnWidths[_columnWidths.length - 1] = DELETE_BUTTON_WIDTH;\n    }\n    const columnWidthSpecs = getWidthSpecs(columns);\n    const columnOffset = state.hasSelectors ? 1 : 0;\n    for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) {\n        const thIndex = columnIndex + columnOffset;\n        const { minWidth, maxWidth } = columnWidthSpecs[columnIndex];\n        if (_columnWidths[thIndex] < minWidth) {\n            _columnWidths[thIndex] = minWidth;\n        } else if (maxWidth && _columnWidths[thIndex] > maxWidth) {\n            _columnWidths[thIndex] = maxWidth;\n        }\n    }\n\n    // Expand/shrink columns for the table to fill 100% of available space\n    const totalWidth = _columnWidths.reduce((tot, width) => tot + width, 0);\n    let diff = totalWidth - allowedWidth;\n    if (diff >= 1) {\n        // Case 1: table overflows its parent => shrink some columns\n        const shrinkableColumns = [];\n        let totalAvailableSpace = 0; // total space we can gain by shrinking columns\n        for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) {\n            const thIndex = columnIndex + columnOffset;\n            const { minWidth, canShrink } = columnWidthSpecs[columnIndex];\n            if (_columnWidths[thIndex] > minWidth && canShrink) {\n                shrinkableColumns.push({ thIndex, minWidth });\n                totalAvailableSpace += _columnWidths[thIndex] - minWidth;\n            }\n        }\n        if (diff > totalAvailableSpace) {\n            // We can't find enough space => set all columns to their min width, and there'll be an\n            // horizontal scrollbar\n            for (const { thIndex, minWidth } of shrinkableColumns) {\n                _columnWidths[thIndex] = minWidth;\n            }\n        } else {\n            // There's enough available space among shrinkable columns => shrink them uniformly\n            let remainingColumnsToShrink = shrinkableColumns.length;\n            while (diff >= 1) {\n                const colDiff = diff / remainingColumnsToShrink;\n                for (const { thIndex, minWidth } of shrinkableColumns) {\n                    const currentWidth = _columnWidths[thIndex];\n                    if (currentWidth === minWidth) {\n                        continue;\n                    }\n                    const newWidth = Math.max(currentWidth - colDiff, minWidth);\n                    diff -= currentWidth - newWidth;\n                    _columnWidths[thIndex] = newWidth;\n                    if (newWidth === minWidth) {\n                        remainingColumnsToShrink--;\n                    }\n                }\n            }\n        }\n    } else if (diff <= -1) {\n        // Case 2: table is narrower than its parent => expand some columns\n        diff = -diff; // for better readability\n        const expandableColumns = [];\n        for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) {\n            const thIndex = columnIndex + columnOffset;\n            const maxWidth = columnWidthSpecs[columnIndex].maxWidth;\n            if (!maxWidth || _columnWidths[thIndex] < maxWidth) {\n                expandableColumns.push({ thIndex, maxWidth });\n            }\n        }\n        // Expand all expandable columns uniformly (i.e. at most, expand columns with a maxWidth\n        // to their maxWidth)\n        let remainingExpandableColumns = expandableColumns.length;\n        while (diff >= 1 && remainingExpandableColumns > 0) {\n            const colDiff = diff / remainingExpandableColumns;\n            for (const { thIndex, maxWidth } of expandableColumns) {\n                const currentWidth = _columnWidths[thIndex];\n                const newWidth = Math.min(currentWidth + colDiff, maxWidth || Number.MAX_VALUE);\n                diff -= newWidth - currentWidth;\n                _columnWidths[thIndex] = newWidth;\n                if (newWidth === maxWidth) {\n                    remainingExpandableColumns--;\n                }\n            }\n        }\n        if (diff >= 1) {\n            // All columns have a maxWidth and have been expanded to their max => expand them more\n            for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) {\n                const thIndex = columnIndex + columnOffset;\n                _columnWidths[thIndex] += diff / columns.length;\n            }\n        }\n    }\n    return _columnWidths;\n}\n\n/**\n * Returns for each column its minimal and (if any) maximal widths.\n *\n * @param {Object[]} columns\n * @returns {Object[]} each entry in this array has a minWidth and optionally a maxWidth key\n */\nfunction getWidthSpecs(columns) {\n    return columns.map((column) => {\n        let minWidth;\n        let maxWidth;\n        if (column.attrs && column.attrs.width) {\n            minWidth = maxWidth = parseInt(column.attrs.width.split(\"px\")[0]);\n        } else {\n            let width;\n            if (column.type === \"field\") {\n                if (column.field.listViewWidth) {\n                    width = column.field.listViewWidth;\n                    if (typeof width === \"function\") {\n                        width = width({ type: column.fieldType, hasLabel: column.hasLabel });\n                    }\n                } else {\n                    width = FIELD_WIDTHS[column.widget || column.fieldType];\n                }\n            } else if (column.type === \"widget\") {\n                width = column.widget.listViewWidth;\n            }\n            if (width) {\n                minWidth = Array.isArray(width) ? width[0] : width;\n                maxWidth = Array.isArray(width) ? width[1] : width;\n            } else {\n                minWidth = DEFAULT_MIN_WIDTH;\n            }\n        }\n        return { minWidth, maxWidth, canShrink: column.type === \"field\" };\n    });\n}\n\n/**\n * Given an html element, returns the sum of its left and right padding.\n *\n * @param {HTMLElement} el\n * @returns {Number}\n */\nfunction getHorizontalPadding(el) {\n    const { paddingLeft, paddingRight } = getComputedStyle(el);\n    return parseFloat(paddingLeft) + parseFloat(paddingRight);\n}\n\nexport function useMagicColumnWidths(tableRef, getState) {\n    const renderer = useComponent();\n    let columnWidths = null;\n    let allowedWidth = 0;\n    let hasAlwaysBeenEmpty = true;\n    let parentWidthFixed = false;\n    let hash;\n    let _resizing = false;\n\n    /**\n     * Apply the column widths in the DOM. If necessary, compute them first (e.g. if they haven't\n     * been computed yet, or if columns have changed).\n     *\n     * Note: the following code manipulates the DOM directly to avoid having to wait for a\n     * render + patch which would occur on the next frame and cause flickering.\n     */\n    function forceColumnWidths() {\n        const table = tableRef.el;\n        const headers = [...table.querySelectorAll(\"thead th\")];\n        const state = getState();\n\n        // Generate a hash to be able to detect when the columns change\n        const columns = state.columns;\n        // The last part of the hash is there to detect that static columns changed (typically, the\n        // selector column, which isn't displayed on small screens)\n        const nextHash = `${columns.map((column) => column.id).join(\"/\")}/${headers.length}`;\n        if (nextHash !== hash) {\n            hash = nextHash;\n            resetWidths();\n        }\n        // If the table has always been empty until now, and it now contains records, we want to\n        // recompute the widths based on the records (typical case: we removed a filter).\n        // Exception: we were in an empty editable list, and we just added a first record.\n        if (hasAlwaysBeenEmpty && !state.isEmpty) {\n            hasAlwaysBeenEmpty = false;\n            const rows = table.querySelectorAll(\".o_data_row\");\n            if (rows.length !== 1 || !rows[0].classList.contains(\"o_selected_row\")) {\n                resetWidths();\n            }\n        }\n\n        const parentPadding = getHorizontalPadding(table.parentNode);\n        const cellPaddings = headers.map((th) => getHorizontalPadding(th));\n        const totalCellPadding = cellPaddings.reduce((total, padding) => padding + total, 0);\n        const nextAllowedWidth = table.parentNode.clientWidth - parentPadding - totalCellPadding;\n        const allowedWidthDiff = Math.abs(allowedWidth - nextAllowedWidth);\n        allowedWidth = nextAllowedWidth;\n\n        // When a vertical scrollbar appears/disappears, it may (depending on the browser/os) change\n        // the available width. When it does, we want to keep the current widths, but tweak them a\n        // little bit s.t. the table fits in the new available space.\n        if (!columnWidths || allowedWidthDiff > 0) {\n            columnWidths = computeWidths(table, state, allowedWidth, columnWidths);\n        }\n\n        // Set the computed widths in the DOM.\n        table.style.tableLayout = \"fixed\";\n        headers.forEach((th, index) => {\n            th.style.width = `${Math.floor(columnWidths[index] + cellPaddings[index])}px`;\n        });\n    }\n\n    /**\n     * Resets the widths. After next patch, ideal widths will be recomputed.\n     */\n    function resetWidths() {\n        columnWidths = null;\n        // Unset widths that might have been set on the table by resizing a column\n        tableRef.el.style.width = null;\n        if (parentWidthFixed) {\n            tableRef.el.parentElement.style.width = null;\n        }\n    }\n\n    /**\n     * Handles the resize feature on the column headers\n     *\n     * @private\n     * @param {MouseEvent} ev\n     */\n    function onStartResize(ev) {\n        _resizing = true;\n        const table = tableRef.el;\n        const th = ev.target.closest(\"th\");\n        const handler = th.querySelector(\".o_resize\");\n        table.style.width = `${Math.floor(table.getBoundingClientRect().width)}px`;\n        const thPosition = [...th.parentNode.children].indexOf(th);\n        const resizingColumnElements = [...table.getElementsByTagName(\"tr\")]\n            .filter((tr) => tr.children.length === th.parentNode.children.length)\n            .map((tr) => tr.children[thPosition]);\n        const initialX = ev.clientX;\n        const initialWidth = th.getBoundingClientRect().width;\n        const initialTableWidth = table.getBoundingClientRect().width;\n        const resizeStoppingEvents = [\"keydown\", \"pointerdown\", \"pointerup\"];\n\n        // Fix the width so that if the resize overflows, it doesn't affect the layout of the parent\n        if (!table.parentElement.style.width) {\n            parentWidthFixed = true;\n            table.parentElement.style.width = `${Math.floor(\n                table.parentElement.getBoundingClientRect().width\n            )}px`;\n        }\n\n        // Apply classes to table and selected column\n        table.classList.add(\"o_resizing\");\n        for (const el of resizingColumnElements) {\n            el.classList.add(\"o_column_resizing\");\n            handler.classList.add(\"bg-primary\", \"opacity-100\");\n            handler.classList.remove(\"bg-black-25\", \"opacity-50-hover\");\n        }\n        // Mousemove event : resize header\n        const resizeHeader = (ev) => {\n            ev.preventDefault();\n            ev.stopPropagation();\n            const delta = ev.clientX - initialX;\n            const newWidth = Math.max(10, initialWidth + delta);\n            const tableDelta = newWidth - initialWidth;\n            th.style.width = `${Math.floor(newWidth)}px`;\n            table.style.width = `${Math.floor(initialTableWidth + tableDelta)}px`;\n        };\n        window.addEventListener(\"pointermove\", resizeHeader);\n\n        // Mouse or keyboard events : stop resize\n        const stopResize = (ev) => {\n            _resizing = false;\n\n            // Store current column widths to freeze them\n            const headers = [...table.querySelectorAll(\"thead th\")];\n            columnWidths = headers.map((th) => {\n                return th.getBoundingClientRect().width - getHorizontalPadding(th);\n            });\n\n            // Ignores the 'left mouse button down' event as it used to start resizing\n            if (ev.type === \"pointerdown\" && ev.button === 0) {\n                return;\n            }\n            ev.preventDefault();\n            ev.stopPropagation();\n\n            table.classList.remove(\"o_resizing\");\n            for (const el of resizingColumnElements) {\n                el.classList.remove(\"o_column_resizing\");\n                handler.classList.remove(\"bg-primary\", \"opacity-100\");\n                handler.classList.add(\"bg-black-25\", \"opacity-50-hover\");\n            }\n\n            window.removeEventListener(\"pointermove\", resizeHeader);\n            for (const eventType of resizeStoppingEvents) {\n                window.removeEventListener(eventType, stopResize);\n            }\n\n            // We remove the focus to make sure that the there is no focus inside\n            // the tr.  If that is the case, there is some css to darken the whole\n            // thead, and it looks quite weird with the small css hover effect.\n            document.activeElement.blur();\n        };\n        // We have to listen to several events to properly stop the resizing function. Those are:\n        // - pointerdown (e.g. pressing right click)\n        // - pointerup : logical flow of the resizing feature (drag & drop)\n        // - keydown : (e.g. pressing 'Alt' + 'Tab' or 'Windows' key)\n        for (const eventType of resizeStoppingEvents) {\n            window.addEventListener(eventType, stopResize);\n        }\n    }\n\n    // Side effects\n    if (renderer.constructor.useMagicColumnWidths) {\n        useEffect(forceColumnWidths);\n        const debouncedResizeCallback = useDebounced(() => {\n            resetWidths();\n            forceColumnWidths();\n        }, 200);\n        useExternalListener(window, \"resize\", debouncedResizeCallback);\n    }\n\n    // API\n    return {\n        get resizing() {\n            return _resizing;\n        },\n        onStartResize,\n    };\n}\n", "import { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { STATIC_ACTIONS_GROUP_NUMBER } from \"@web/search/action_menus/action_menus\";\n\nimport { Component } from \"@odoo/owl\";\n\nconst cogMenuRegistry = registry.category(\"cogMenu\");\n\n/**\n * 'Export All' menu\n *\n * This component is used to export all the records for particular model.\n * @extends Component\n */\nexport class ExportAll extends Component {\n    static template = \"web.ExportAll\";\n    static components = { DropdownItem };\n    static props = {};\n\n    //---------------------------------------------------------------------\n    // Protected\n    //---------------------------------------------------------------------\n\n    async onDirectExportData() {\n        this.env.searchModel.trigger(\"direct-export-data\");\n    }\n}\n\nexport const exportAllItem = {\n    Component: ExportAll,\n    groupNumber: STATIC_ACTIONS_GROUP_NUMBER,\n    isDisplayed: async (env) =>\n        env.config.viewType === \"list\" &&\n        !env.model.root.selection.length &&\n        (await user.hasGroup(\"base.group_allow_export\")) &&\n        exprToBoolean(env.config.viewArch.getAttribute(\"export_xlsx\"), true),\n};\n\ncogMenuRegistry.add(\"export-all-menu\", exportAllItem, { sequence: 10 });\n", "import { exprToBoolean } from \"@web/core/utils/strings\";\nimport { visitXML } from \"@web/core/utils/xml\";\nimport { combineModifiers } from \"@web/model/relational_model/utils\";\nimport { stringToOrderBy } from \"@web/search/utils/order_by\";\nimport { Field } from \"@web/views/fields/field\";\nimport { getActiveActions, getDecoration, processButton } from \"@web/views/utils\";\nimport { encodeObjectForTemplate } from \"@web/views/view_compiler\";\nimport { Widget } from \"@web/views/widgets/widget\";\n\nexport class GroupListArchParser {\n    parse(arch, models, modelName, jsClass) {\n        const fieldNodes = {};\n        const fieldNextIds = {};\n        const buttons = [];\n        let buttonId = 0;\n        visitXML(arch, (node) => {\n            if (node.tagName === \"button\") {\n                buttons.push({\n                    ...processButton(node),\n                    id: buttonId++,\n                });\n                return false;\n            } else if (node.tagName === \"field\") {\n                const fieldInfo = Field.parseFieldNode(node, models, modelName, \"list\", jsClass);\n                if (!(fieldInfo.name in fieldNextIds)) {\n                    fieldNextIds[fieldInfo.name] = 0;\n                }\n                const fieldId = `${fieldInfo.name}_${fieldNextIds[fieldInfo.name]++}`;\n                fieldNodes[fieldId] = fieldInfo;\n                node.setAttribute(\"field_id\", fieldId);\n                return false;\n            }\n        });\n        return { fieldNodes, buttons };\n    }\n}\n\nexport class ListArchParser {\n    parseFieldNode(node, models, modelName) {\n        return Field.parseFieldNode(node, models, modelName, \"list\");\n    }\n\n    parseWidgetNode(node, models, modelName) {\n        return Widget.parseWidgetNode(node);\n    }\n\n    processButton(node) {\n        return processButton(node);\n    }\n\n    parse(xmlDoc, models, modelName) {\n        const fieldNodes = {};\n        const widgetNodes = {};\n        let widgetNextId = 0;\n        const columns = [];\n        const fields = models[modelName].fields;\n        let buttonId = 0;\n        const groupBy = {\n            buttons: {},\n            fields: {},\n        };\n        let headerButtons = [];\n        const creates = [];\n        const groupListArchParser = new GroupListArchParser();\n        let buttonGroup;\n        let handleField = null;\n        const treeAttr = {};\n        let nextId = 0;\n        const fieldNextIds = {};\n        visitXML(xmlDoc, (node) => {\n            if (node.tagName !== \"button\") {\n                buttonGroup = undefined;\n            }\n            if (node.tagName === \"button\") {\n                const button = {\n                    ...this.processButton(node),\n                    defaultRank: \"btn-link\",\n                    type: \"button\",\n                    id: buttonId++,\n                };\n                const width = button.attrs.width;\n                if (buttonGroup && !width) {\n                    buttonGroup.buttons.push(button);\n                    buttonGroup.column_invisible = combineModifiers(\n                        buttonGroup.column_invisible,\n                        node.getAttribute(\"column_invisible\"),\n                        \"AND\"\n                    );\n                } else {\n                    buttonGroup = {\n                        id: `column_${nextId++}`,\n                        type: \"button_group\",\n                        buttons: [button],\n                        hasLabel: false,\n                        column_invisible: node.getAttribute(\"column_invisible\"),\n                    };\n                    columns.push(buttonGroup);\n                    if (width) {\n                        buttonGroup.attrs = { width };\n                        buttonGroup = undefined;\n                    }\n                }\n            } else if (node.tagName === \"field\") {\n                const fieldInfo = this.parseFieldNode(node, models, modelName);\n                if (!(fieldInfo.name in fieldNextIds)) {\n                    fieldNextIds[fieldInfo.name] = 0;\n                }\n                const fieldId = `${fieldInfo.name}_${fieldNextIds[fieldInfo.name]++}`;\n                fieldNodes[fieldId] = fieldInfo;\n                node.setAttribute(\"field_id\", fieldId);\n                if (fieldInfo.isHandle) {\n                    handleField = fieldInfo.name;\n                }\n                const label = fieldInfo.field.label;\n                columns.push({\n                    ...fieldInfo,\n                    id: `column_${nextId++}`,\n                    className: node.getAttribute(\"class\"), // for oe_edit_only and oe_read_only\n                    optional: node.getAttribute(\"optional\") || false,\n                    type: \"field\",\n                    fieldType: fieldInfo.type,\n                    hasLabel: !(\n                        fieldInfo.field.label === false ||\n                        exprToBoolean(fieldInfo.attrs.nolabel) === true\n                    ),\n                    label: (fieldInfo.widget && label && label.toString()) || fieldInfo.string,\n                });\n                return false;\n            } else if (node.tagName === \"widget\") {\n                const widgetInfo = this.parseWidgetNode(node);\n                const widgetId = `widget_${++widgetNextId}`;\n                widgetNodes[widgetId] = widgetInfo;\n                node.setAttribute(\"widget_id\", widgetId);\n\n                const widgetProps = {\n                    name: widgetInfo.name,\n                    // FIXME: this is dumb, we encode it into a weird object so that the widget\n                    // can decode it later...\n                    node: encodeObjectForTemplate({ attrs: widgetInfo.attrs }).slice(1, -1),\n                    className: node.getAttribute(\"class\") || \"\",\n                    widgetInfo,\n                };\n                columns.push({\n                    ...widgetInfo,\n                    props: widgetProps,\n                    id: `column_${nextId++}`,\n                    type: \"widget\",\n                });\n            } else if (node.tagName === \"groupby\" && node.getAttribute(\"name\")) {\n                const fieldName = node.getAttribute(\"name\");\n                const coModelName = fields[fieldName].relation;\n                const groupByArchInfo = groupListArchParser.parse(node, models, coModelName);\n                groupBy.buttons[fieldName] = groupByArchInfo.buttons;\n                groupBy.fields[fieldName] = {\n                    fieldNodes: groupByArchInfo.fieldNodes,\n                    fields: models[coModelName].fields,\n                };\n                return false;\n            } else if (node.tagName === \"header\") {\n                // AAB: not sure we need to handle invisible=\"True\" button as the usecase seems way\n                // less relevant than for fields (so for buttons, relying on the modifiers logic\n                // that applies later on could be enough, even if the value is always true)\n                headerButtons = [...node.children].map((node) => ({\n                    ...this.processButton(node),\n                    type: \"button\",\n                    id: buttonId++,\n                }));\n                return false;\n            } else if (node.tagName === \"control\") {\n                for (const childNode of node.children) {\n                    if (childNode.tagName === \"button\") {\n                        creates.push({\n                            type: \"button\",\n                            ...processButton(childNode),\n                        });\n                    } else if (childNode.tagName === \"create\") {\n                        creates.push({\n                            type: \"create\",\n                            context: childNode.getAttribute(\"context\"),\n                            string: childNode.getAttribute(\"string\"),\n                        });\n                    }\n                }\n                return false;\n            } else if (\"list\" === node.tagName) {\n                const activeActions = {\n                    ...getActiveActions(xmlDoc),\n                    exportXlsx: exprToBoolean(xmlDoc.getAttribute(\"export_xlsx\"), true),\n                };\n                treeAttr.activeActions = activeActions;\n\n                treeAttr.className = xmlDoc.getAttribute(\"class\") || null;\n                treeAttr.editable = xmlDoc.getAttribute(\"editable\");\n                treeAttr.multiEdit = activeActions.edit\n                    ? exprToBoolean(node.getAttribute(\"multi_edit\") || \"\")\n                    : false;\n\n                treeAttr.openFormView = treeAttr.editable\n                    ? exprToBoolean(xmlDoc.getAttribute(\"open_form_view\") || \"\")\n                    : false;\n\n                const limitAttr = node.getAttribute(\"limit\");\n                treeAttr.limit = limitAttr && parseInt(limitAttr, 10);\n\n                const countLimitAttr = node.getAttribute(\"count_limit\");\n                treeAttr.countLimit = countLimitAttr && parseInt(countLimitAttr, 10);\n\n                const groupsLimitAttr = node.getAttribute(\"groups_limit\");\n                treeAttr.groupsLimit = groupsLimitAttr && parseInt(groupsLimitAttr, 10);\n\n                treeAttr.noOpen = exprToBoolean(node.getAttribute(\"no_open\") || \"\");\n                treeAttr.rawExpand = xmlDoc.getAttribute(\"expand\");\n                treeAttr.decorations = getDecoration(xmlDoc);\n\n                treeAttr.defaultGroupBy = xmlDoc.getAttribute(\"default_group_by\");\n                treeAttr.defaultOrder = stringToOrderBy(\n                    xmlDoc.getAttribute(\"default_order\") || null\n                );\n\n                // custom open action when clicking on record row\n                const action = xmlDoc.getAttribute(\"action\");\n                const type = xmlDoc.getAttribute(\"type\");\n                treeAttr.openAction = action && type ? { action, type } : null;\n            }\n        });\n\n        if (!treeAttr.defaultOrder.length && handleField) {\n            const handleFieldSort = `${handleField}, id`;\n            treeAttr.defaultOrder = stringToOrderBy(handleFieldSort);\n        }\n\n        return {\n            creates,\n            headerButtons,\n            fieldNodes,\n            widgetNodes,\n            columns,\n            groupBy,\n            xmlDoc,\n            ...treeAttr,\n        };\n    }\n}\n", "import { CogMenu } from \"../../search/cog_menu/cog_menu\";\n\nexport class ListCogMenu extends CogMenu {\n    static template = \"web.ListCogMenu\";\n    static props = {\n        ...CogMenu.props,\n        hasSelectedRecords: { type: Number, optional: true },\n        slots: { type: Object, optional: true },\n    };\n    _registryItems() {\n        return this.props.hasSelectedRecords ? [] : super._registryItems();\n    }\n}\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Field } from \"@web/views/fields/field\";\nimport { useAutofocus } from \"@web/core/utils/hooks\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class ListConfirmationDialog extends Component {\n    static template = \"web.ListView.ConfirmationModal\";\n    static components = { Dialog, Field };\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        confirm: { type: Function, optional: true },\n        cancel: { type: Function, optional: true },\n        isDomainSelected: Boolean,\n        fields: Object,\n        nbRecords: Number,\n        nbValidRecords: Number,\n        record: Object,\n    };\n    static defaultProps = {\n        title: _t(\"Confirmation\"),\n    };\n\n    setup() {\n        useAutofocus();\n    }\n\n    _cancel() {\n        if (this.props.cancel) {\n            this.props.cancel();\n        }\n        this.props.close();\n    }\n\n    async _confirm() {\n        if (this.props.confirm) {\n            await this.props.confirm();\n        }\n        this.props.close();\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport {\n    deleteConfirmationMessage,\n    ConfirmationDialog,\n} from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { download } from \"@web/core/network/download\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { evaluateExpr, evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { user } from \"@web/core/user\";\nimport { unique } from \"@web/core/utils/arrays\";\nimport { useService, useBus } from \"@web/core/utils/hooks\";\nimport { omit } from \"@web/core/utils/objects\";\nimport { useSetupAction } from \"@web/search/action_hook\";\nimport { ActionMenus, STATIC_ACTIONS_GROUP_NUMBER } from \"@web/search/action_menus/action_menus\";\nimport { Layout } from \"@web/search/layout\";\nimport { usePager } from \"@web/search/pager_hook\";\nimport { useModelWithSampleData } from \"@web/model/model\";\nimport { DynamicRecordList } from \"@web/model/relational_model/dynamic_record_list\";\nimport { extractFieldsFromArchInfo } from \"@web/model/relational_model/utils\";\nimport { standardViewProps } from \"@web/views/standard_view_props\";\nimport { MultiRecordViewButton } from \"@web/views/view_button/multi_record_view_button\";\nimport { ViewButton } from \"@web/views/view_button/view_button\";\nimport { executeButtonCallback, useViewButtons } from \"@web/views/view_button/view_button_hook\";\nimport { ExportDataDialog } from \"@web/views/view_dialogs/export_data_dialog\";\nimport { ListConfirmationDialog } from \"./list_confirmation_dialog\";\nimport { SearchBar } from \"@web/search/search_bar/search_bar\";\nimport { useSearchBarToggler } from \"@web/search/search_bar/search_bar_toggler\";\nimport { session } from \"@web/session\";\nimport { ListCogMenu } from \"./list_cog_menu\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\n\nimport {\n    Component,\n    onMounted,\n    onWillPatch,\n    onWillRender,\n    onWillStart,\n    useEffect,\n    useRef,\n    useState,\n    useSubEnv,\n} from \"@odoo/owl\";\n\n// -----------------------------------------------------------------------------\n\nexport class ListController extends Component {\n    static template = `web.ListView`;\n    static components = {\n        ActionMenus,\n        Layout,\n        ViewButton,\n        MultiRecordViewButton,\n        SearchBar,\n        CogMenu: ListCogMenu,\n        DropdownItem,\n    };\n    static props = {\n        ...standardViewProps,\n        allowSelectors: { type: Boolean, optional: true },\n        editable: { type: Boolean, optional: true },\n        onSelectionChanged: { type: Function, optional: true },\n        showButtons: { type: Boolean, optional: true },\n        Model: Function,\n        Renderer: Function,\n        buttonTemplate: String,\n        archInfo: Object,\n    };\n    static defaultProps = {\n        allowSelectors: true,\n        createRecord: () => {},\n        editable: true,\n        selectRecord: () => {},\n        showButtons: true,\n    };\n\n    setup() {\n        this.actionService = useService(\"action\");\n        this.dialogService = useService(\"dialog\");\n        this.rootRef = useRef(\"root\");\n\n        this.archInfo = this.props.archInfo;\n        this.activeActions = this.archInfo.activeActions;\n        this.editable =\n            this.activeActions.edit && this.props.editable ? this.archInfo.editable : false;\n        this.onOpenFormView = this.openRecord.bind(this);\n        this.hasOpenFormViewButton = this.editable ? this.archInfo.openFormView : false;\n        this.model = useState(useModelWithSampleData(this.props.Model, this.modelParams));\n\n        // In multi edition, we save or notify invalidity directly when a field is updated, which\n        // occurs on the change event for input fields. But we don't want to do it when clicking on\n        // \"Discard\". So we set a flag on mousedown (which triggers the update) to block the multi\n        // save or invalid notification.\n        // However, if the mouseup (and click) is done outside \"Discard\", we finally want to do it.\n        // We use `nextActionAfterMouseup` for this purpose: it registers a callback to execute if\n        // the mouseup following a mousedown on \"Discard\" isn't done on \"Discard\".\n        this.hasMousedownDiscard = false;\n        this.nextActionAfterMouseup = null;\n\n        this.optionalActiveFields = {};\n\n        this.editedRecord = null;\n        onWillRender(() => {\n            this.editedRecord = this.model.root.editedRecord;\n        });\n\n        onWillStart(async () => {\n            this.isExportEnable = await user.hasGroup(\"base.group_allow_export\");\n        });\n\n        onMounted(() => {\n            const { rendererScrollPositions } = this.props.state || {};\n            if (rendererScrollPositions) {\n                const renderer = this.rootRef.el.querySelector(\".o_list_renderer\");\n                renderer.scrollLeft = rendererScrollPositions.left;\n                renderer.scrollTop = rendererScrollPositions.top;\n            }\n        });\n\n        this.archiveEnabled =\n            \"active\" in this.props.fields\n                ? !this.props.fields.active.readonly\n                : \"x_active\" in this.props.fields\n                ? !this.props.fields.x_active.readonly\n                : false;\n        useSubEnv({ model: this.model }); // do this in useModelWithSampleData?\n        useViewButtons(this.rootRef, {\n            beforeExecuteAction: this.beforeExecuteActionButton.bind(this),\n            afterExecuteAction: this.afterExecuteActionButton.bind(this),\n            reload: () => this.model.load(),\n        });\n        useSetupAction({\n            rootRef: this.rootRef,\n            beforeLeave: async () => {\n                return this.model.root.leaveEditMode();\n            },\n            beforeUnload: async (ev) => {\n                if (this.editedRecord) {\n                    const isValid = await this.editedRecord.urgentSave();\n                    if (!isValid) {\n                        ev.preventDefault();\n                        ev.returnValue = \"Unsaved changes\";\n                    }\n                }\n            },\n            getLocalState: () => {\n                const renderer = this.rootRef.el.querySelector(\".o_list_renderer\");\n                return {\n                    modelState: this.model.exportState(),\n                    rendererScrollPositions: {\n                        left: renderer.scrollLeft,\n                        top: renderer.scrollTop,\n                    },\n                };\n            },\n            getOrderBy: () => {\n                return this.model.root.orderBy;\n            },\n        });\n\n        usePager(() => {\n            if (this.model.useSampleModel) {\n                return;\n            }\n            const { count, hasLimitedCount, isGrouped, limit, offset } = this.model.root;\n            return {\n                offset: offset,\n                limit: limit,\n                total: count,\n                onUpdate: async ({ offset, limit }, hasNavigated) => {\n                    if (this.editedRecord) {\n                        if (!(await this.editedRecord.save())) {\n                            return;\n                        }\n                    }\n                    await this.model.root.load({ limit, offset });\n                    if (hasNavigated) {\n                        this.onPageChangeScroll();\n                    }\n                },\n                updateTotal:\n                    !isGrouped && hasLimitedCount ? () => this.model.root.fetchCount() : undefined,\n            };\n        });\n\n        useEffect(\n            () => {\n                if (this.props.onSelectionChanged) {\n                    const resIds = this.model.root.selection.map((record) => record.resId);\n                    this.props.onSelectionChanged(resIds);\n                }\n            },\n            () => [this.model.root.selection.length]\n        );\n        this.searchBarToggler = useSearchBarToggler();\n        this.firstLoad = true;\n        onWillPatch(() => {\n            this.firstLoad = false;\n        });\n        useBus(this.env.searchModel, \"direct-export-data\", this.onDirectExportData.bind(this));\n    }\n\n    get modelParams() {\n        const { defaultGroupBy, rawExpand } = this.archInfo;\n        const { activeFields, fields } = extractFieldsFromArchInfo(\n            this.archInfo,\n            this.props.fields\n        );\n        const groupByInfo = {};\n        for (const fieldName in this.archInfo.groupBy.fields) {\n            const fieldNodes = this.archInfo.groupBy.fields[fieldName].fieldNodes;\n            const fields = this.archInfo.groupBy.fields[fieldName].fields;\n            groupByInfo[fieldName] = extractFieldsFromArchInfo({ fieldNodes }, fields);\n        }\n\n        const modelConfig = this.props.state?.modelState?.config || {\n            resModel: this.props.resModel,\n            fields,\n            activeFields,\n            openGroupsByDefault: rawExpand ? evaluateExpr(rawExpand, this.props.context) : false,\n        };\n\n        return {\n            config: modelConfig,\n            state: this.props.state?.modelState,\n            groupByInfo,\n            limit: this.archInfo.limit || this.props.limit,\n            countLimit: this.archInfo.countLimit,\n            defaultOrderBy: this.archInfo.defaultOrder,\n            defaultGroupBy: this.props.searchMenuTypes.includes(\"groupBy\") ? defaultGroupBy : false,\n            groupsLimit: this.archInfo.groupsLimit,\n            multiEdit: this.archInfo.multiEdit,\n            activeIdsLimit: session.active_ids_limit,\n            hooks: {\n                onRecordSaved: this.onRecordSaved.bind(this),\n                onWillSaveRecord: this.onWillSaveRecord.bind(this),\n                onWillSaveMulti: this.onWillSaveMulti.bind(this),\n                onWillSetInvalidField: this.onWillSetInvalidField.bind(this),\n            },\n        };\n    }\n\n    get actionMenuProps() {\n        return {\n            getActiveIds: () => this.model.root.selection.map((r) => r.resId),\n            context: this.props.context,\n            domain: this.props.domain,\n            items: this.actionMenuItems,\n            isDomainSelected: this.model.root.isDomainSelected,\n            resModel: this.model.root.resModel,\n            onActionExecuted: () => this.model.load(),\n        };\n    }\n\n    /**\n     * onRecordSaved is a callBack that will be executed after the save\n     * if it was done. It will therefore not be executed if the record\n     * is invalid or if a server error is thrown.\n     * @param {Record} record\n     */\n    async onRecordSaved(record) {}\n\n    /**\n     * onWillSaveRecord is a callBack that will be executed before the\n     * record save if the record is valid if the record is valid.\n     * If it returns false, it will prevent the save.\n     * @param {Record} record\n     */\n    async onWillSaveRecord(record) {}\n\n    async createRecord({ group } = {}) {\n        const list = (group && group.list) || this.model.root;\n        if (this.editable && !list.isGrouped) {\n            if (!(list instanceof DynamicRecordList)) {\n                throw new Error(\"List should be a DynamicRecordList\");\n            }\n            await list.leaveEditMode();\n            if (!list.editedRecord) {\n                await (group || list).addNewRecord(this.editable === \"top\");\n            }\n            this.render();\n        } else {\n            await this.props.createRecord();\n        }\n    }\n\n    async openRecord(record, force = false) {\n        const dirty = await record.isDirty();\n        if (dirty) {\n            await record.save();\n        }\n        if (this.archInfo.openAction) {\n            this.actionService.doActionButton({\n                name: this.archInfo.openAction.action,\n                type: this.archInfo.openAction.type,\n                resModel: record.resModel,\n                resId: record.resId,\n                resIds: record.resIds,\n                context: record.context,\n                onClose: async () => {\n                    await record.model.root.load();\n                },\n            });\n        } else {\n            const activeIds = this.model.root.records.map((datapoint) => datapoint.resId);\n            this.props.selectRecord(record.resId, { activeIds, force });\n        }\n    }\n\n    async onClickCreate() {\n        return executeButtonCallback(this.rootRef.el, () => this.createRecord());\n    }\n\n    async onClickDiscard() {\n        return executeButtonCallback(this.rootRef.el, () =>\n            this.model.root.leaveEditMode({ discard: true })\n        );\n    }\n\n    async onClickSave() {\n        return executeButtonCallback(this.rootRef.el, async () => {\n            const saved = await this.editedRecord.save();\n            if (saved) {\n                await this.model.root.leaveEditMode();\n            }\n        });\n    }\n\n    onMouseDownDiscard(mouseDownEvent) {\n        this.hasMousedownDiscard = true;\n        document.addEventListener(\n            \"mouseup\",\n            (mouseUpEvent) => {\n                this.hasMousedownDiscard = false;\n                if (mouseUpEvent.target !== mouseDownEvent.target) {\n                    if (this.nextActionAfterMouseup) {\n                        this.nextActionAfterMouseup();\n                    }\n                }\n                this.nextActionAfterMouseup = null;\n            },\n            { capture: true, once: true }\n        );\n    }\n\n    onPageChangeScroll() {\n        if (this.rootRef && this.rootRef.el) {\n            if (this.env.isSmall) {\n                this.rootRef.el.scrollTop = 0;\n            } else {\n                this.rootRef.el.querySelector(\".o_content .o_list_renderer\").scrollTop = 0;\n            }\n        }\n    }\n\n    getSelectedResIds() {\n        return this.model.root.getResIds(true);\n    }\n\n    getStaticActionMenuItems() {\n        return {\n            export: {\n                isAvailable: () => this.isExportEnable,\n                sequence: 10,\n                icon: \"fa fa-upload\",\n                description: _t(\"Export\"),\n                callback: () => this.onExportData(),\n            },\n            archive: {\n                isAvailable: () => this.archiveEnabled,\n                sequence: 20,\n                icon: \"oi oi-archive\",\n                description: _t(\"Archive\"),\n                callback: () => {\n                    this.dialogService.add(ConfirmationDialog, this.archiveDialogProps);\n                },\n            },\n            unarchive: {\n                isAvailable: () => this.archiveEnabled,\n                sequence: 30,\n                icon: \"oi oi-unarchive\",\n                description: _t(\"Unarchive\"),\n                callback: () => this.toggleArchiveState(false),\n            },\n            duplicate: {\n                isAvailable: () => this.activeActions.duplicate,\n                sequence: 35,\n                icon: \"fa fa-clone\",\n                description: _t(\"Duplicate\"),\n                callback: () => this.duplicateRecords(),\n            },\n            delete: {\n                isAvailable: () => this.activeActions.delete,\n                sequence: 40,\n                icon: \"fa fa-trash-o\",\n                description: _t(\"Delete\"),\n                callback: () => this.onDeleteSelectedRecords(),\n            },\n        };\n    }\n\n    get archiveDialogProps() {\n        return {\n            body: _t(\"Are you sure that you want to archive all the selected records?\"),\n            confirmLabel: _t(\"Archive\"),\n            confirm: () => {\n                this.toggleArchiveState(true);\n            },\n            cancel: () => {},\n        };\n    }\n\n    get actionMenuItems() {\n        const { actionMenus } = this.props.info;\n        const staticActionItems = Object.entries(this.getStaticActionMenuItems())\n            .filter(([key, item]) => item.isAvailable === undefined || item.isAvailable())\n            .sort(([k1, item1], [k2, item2]) => (item1.sequence || 0) - (item2.sequence || 0))\n            .map(([key, item]) =>\n                Object.assign(\n                    { key, groupNumber: STATIC_ACTIONS_GROUP_NUMBER },\n                    omit(item, \"isAvailable\")\n                )\n            );\n\n        return {\n            action: [...staticActionItems, ...(actionMenus?.action || [])],\n            print: actionMenus?.print,\n        };\n    }\n\n    async onSelectDomain() {\n        await this.model.root.selectDomain(true);\n        if (this.props.onSelectionChanged) {\n            const resIds = await this.model.root.getResIds(true);\n            this.props.onSelectionChanged(resIds);\n        }\n    }\n\n    onUnselectAll() {\n        this.model.root.selection.forEach((record) => {\n            record.toggleSelection(false);\n        });\n        this.model.root.selectDomain(false);\n    }\n\n    evalViewModifier(modifier) {\n        return evaluateBooleanExpr(modifier, this.model.root.evalContext);\n    }\n\n    get className() {\n        return this.props.className;\n    }\n\n    get hasSelectedRecords() {\n        return this.nbSelected || this.isDomainSelected;\n    }\n\n    get nbSelected() {\n        return this.model.root.selection.length;\n    }\n\n    get isPageSelected() {\n        const root = this.model.root;\n        return root.selection.length === root.records.length;\n    }\n\n    get isDomainSelected() {\n        return this.model.root.isDomainSelected;\n    }\n\n    get nbTotal() {\n        const list = this.model.root;\n        return list.isGrouped ? list.recordCount : list.count;\n    }\n\n    get defaultExportList() {\n        return unique(\n            this.props.archInfo.columns\n                .filter((col) => col.type === \"field\")\n                .filter((col) => !col.optional || this.optionalActiveFields[col.name])\n                .filter((col) => !evaluateBooleanExpr(col.column_invisible, this.props.context))\n                .map((col) => this.props.fields[col.name])\n                .filter((field) => field.exportable !== false)\n        );\n    }\n\n    get display() {\n        const { controlPanel } = this.props.display;\n        if (!controlPanel) {\n            return this.props.display;\n        }\n        return {\n            ...this.props.display,\n            controlPanel: {\n                ...controlPanel,\n                layoutActions: !this.hasSelectedRecords,\n            },\n        };\n    }\n\n    async downloadExport(fields, import_compat, format) {\n        let ids = false;\n        if (!this.isDomainSelected) {\n            const resIds = await this.getSelectedResIds();\n            ids = resIds.length > 0 && resIds;\n        }\n        const exportedFields = fields.map((field) => ({\n            name: field.name || field.id,\n            label: field.label || field.string,\n            store: field.store,\n            type: field.field_type || field.type,\n        }));\n        if (import_compat) {\n            exportedFields.unshift({\n                name: \"id\",\n                label: _t(\"External ID\"),\n            });\n        }\n        await download({\n            data: {\n                data: JSON.stringify({\n                    import_compat,\n                    context: this.props.context,\n                    domain: this.model.root.domain,\n                    fields: exportedFields,\n                    groupby: this.model.root.groupBy,\n                    ids,\n                    model: this.model.root.resModel,\n                }),\n            },\n            url: `/web/export/${format}`,\n        });\n    }\n\n    async getExportedFields(model, import_compat, parentParams) {\n        let domain = parentParams ? [] : this.model.root.domain;\n        if (!this.isDomainSelected) {\n            const resIds = await this.getSelectedResIds();\n            const ids = resIds.length > 0 && resIds;\n            domain = [[\"id\", \"in\", ids]];\n        }\n        return await rpc(\"/web/export/get_fields\", {\n            ...parentParams,\n            model,\n            domain,\n            import_compat,\n        });\n    }\n\n    /**\n     * Opens the Export Dialog\n     *\n     * @private\n     */\n    async onExportData() {\n        const dialogProps = {\n            context: this.props.context,\n            defaultExportList: this.defaultExportList,\n            download: this.downloadExport.bind(this),\n            getExportedFields: this.getExportedFields.bind(this),\n            root: this.model.root,\n        };\n        this.dialogService.add(ExportDataDialog, dialogProps);\n    }\n    /**\n     * Export Records in a xls file\n     *\n     * @private\n     */\n    async onDirectExportData() {\n        await this.downloadExport(this.defaultExportList, false, \"xlsx\");\n    }\n    /**\n     * Called when clicking on 'Archive' or 'Unarchive' in the sidebar.\n     *\n     * @private\n     * @param {boolean} archive\n     * @returns {Promise}\n     */\n    async toggleArchiveState(archive) {\n        if (archive) {\n            return this.model.root.archive(true);\n        }\n        return this.model.root.unarchive(true);\n    }\n\n    async duplicateRecords() {\n        return this.model.root.duplicateRecords();\n    }\n\n    get deleteConfirmationDialogProps() {\n        const root = this.model.root;\n        let body = deleteConfirmationMessage;\n        if (root.isDomainSelected || root.selection.length > 1) {\n            body = _t(\"Are you sure you want to delete these records?\");\n        }\n        return {\n            title: _t(\"Bye-bye, record!\"),\n            body,\n            confirmLabel: _t(\"Delete\"),\n            confirm: () => this.model.root.deleteRecords(),\n            cancel: () => {},\n            cancelLabel: _t(\"No, keep it\"),\n        };\n    }\n\n    async onDeleteSelectedRecords() {\n        this.dialogService.add(ConfirmationDialog, this.deleteConfirmationDialogProps);\n    }\n\n    discardSelection() {\n        this.model.root.records.forEach((record) => {\n            record.toggleSelection(false);\n        });\n    }\n\n    async beforeExecuteActionButton(clickParams) {\n        if (clickParams.special !== \"cancel\" && this.editedRecord) {\n            return this.editedRecord.save();\n        }\n    }\n\n    async afterExecuteActionButton(clickParams) {}\n\n    onWillSaveMulti(editedRecord, changes, validSelectedRecords) {\n        if (this.hasMousedownDiscard) {\n            this.nextActionAfterMouseup = () => this.model.root.multiSave(editedRecord);\n            return false;\n        }\n        if (validSelectedRecords.length > 1) {\n            const { isDomainSelected, selection } = this.model.root;\n            return new Promise((resolve) => {\n                const dialogProps = {\n                    confirm: () => resolve(true),\n                    cancel: () => {\n                        if (this.editedRecord) {\n                            this.model.root.leaveEditMode({ discard: true });\n                        } else {\n                            editedRecord.discard();\n                        }\n                        resolve(false);\n                    },\n                    isDomainSelected,\n                    fields: Object.keys(changes).map((fieldName) => {\n                        const fieldNode = Object.values(this.archInfo.fieldNodes).find(\n                            (fieldNode) => fieldNode.name === fieldName\n                        );\n                        const label = fieldNode && fieldNode.string;\n                        return {\n                            name: fieldName,\n                            label: label || editedRecord.fields[fieldName].string,\n                            fieldNode,\n                            widget: fieldNode && fieldNode.widget,\n                        };\n                    }),\n                    nbRecords: selection.length,\n                    nbValidRecords: validSelectedRecords.length,\n                    record: editedRecord,\n                };\n\n                const focusedCellBeforeDialog = document.activeElement.closest(\".o_data_cell\");\n                this.dialogService.add(ListConfirmationDialog, dialogProps, {\n                    onClose: () => {\n                        if (focusedCellBeforeDialog) {\n                            focusedCellBeforeDialog.focus();\n                        }\n                        this.model.root.leaveEditMode({ discard: true });\n                        resolve(false);\n                    },\n                });\n            });\n        }\n        return true;\n    }\n\n    onWillSetInvalidField(record, fieldName) {\n        if (this.hasMousedownDiscard) {\n            this.nextActionAfterMouseup = () => record.setInvalidField(fieldName);\n            return false;\n        }\n        return true;\n    }\n}\n", "import { browser } from \"@web/core/browser/browser\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { Pager } from \"@web/core/pager/pager\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { registry } from \"@web/core/registry\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { useSortable } from \"@web/core/utils/sortable_owl\";\nimport { getTabableElements } from \"@web/core/utils/ui\";\nimport { Field, getPropertyFieldInfo } from \"@web/views/fields/field\";\nimport { getTooltipInfo } from \"@web/views/fields/field_tooltip\";\nimport { getClassNameFromDecoration } from \"@web/views/utils\";\nimport { combineModifiers } from \"@web/model/relational_model/utils\";\nimport { ViewButton } from \"@web/views/view_button/view_button\";\nimport { useBounceButton } from \"@web/views/view_hook\";\nimport { Widget } from \"@web/views/widgets/widget\";\nimport { getFormattedValue } from \"../utils\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { useMagicColumnWidths } from \"./column_width_hook\";\n\nimport {\n    Component,\n    onMounted,\n    onPatched,\n    onWillPatch,\n    onWillRender,\n    useExternalListener,\n    useRef,\n} from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\n\nconst formatters = registry.category(\"formatters\");\n\nconst DEFAULT_GROUP_PAGER_COLSPAN = 1;\n\nconst FIELD_CLASSES = {\n    char: \"o_list_char\",\n    float: \"o_list_number\",\n    integer: \"o_list_number\",\n    monetary: \"o_list_number\",\n    text: \"o_list_text\",\n    many2one: \"o_list_many2one\",\n};\n\n/**\n * @param {HTMLElement} parent\n */\nfunction containsActiveElement(parent) {\n    const { activeElement } = document;\n    return parent !== activeElement && parent.contains(activeElement);\n}\n\n/**\n * @param {HTMLElement} cell\n * @param {number} index\n */\nfunction getElementToFocus(cell, index) {\n    return getTabableElements(cell).at(index) || cell;\n}\n\nexport class ListRenderer extends Component {\n    static template = \"web.ListRenderer\";\n    static rowsTemplate = \"web.ListRenderer.Rows\";\n    static recordRowTemplate = \"web.ListRenderer.RecordRow\";\n    static groupRowTemplate = \"web.ListRenderer.GroupRow\";\n    static useMagicColumnWidths = true;\n    static LONG_TOUCH_THRESHOLD = 400;\n    static components = { DropdownItem, Field, ViewButton, CheckBox, Dropdown, Pager, Widget };\n    static defaultProps = { hasSelectors: false, cycleOnTab: true };\n    static props = [\n        \"activeActions?\",\n        \"list\",\n        \"archInfo\",\n        \"openRecord\",\n        \"onAdd?\",\n        \"cycleOnTab?\",\n        \"allowSelectors?\",\n        \"editable?\",\n        \"onOpenFormView?\",\n        \"hasOpenFormViewButton?\",\n        \"noContentHelp?\",\n        \"nestedKeyOptionalFieldsData?\",\n        \"optionalActiveFields?\",\n    ];\n\n    setup() {\n        this.uiService = useService(\"ui\");\n        this.notificationService = useService(\"notification\");\n        const key = this.createViewKey();\n        this.keyOptionalFields = `optional_fields,${key}`;\n        this.keyDebugOpenView = `debug_open_view,${key}`;\n        this.cellClassByColumn = {};\n        this.groupByButtons = this.props.archInfo.groupBy.buttons;\n        useExternalListener(document, \"click\", this.onGlobalClick.bind(this));\n        this.tableRef = useRef(\"table\");\n\n        this.longTouchTimer = null;\n        this.touchStartMs = 0;\n\n        /**\n         * When resizing columns, it's possible that the pointer is not above the resize\n         * handle (by some few pixel difference). During this scenario, click event\n         * will be triggered on the column title which will reorder the column.\n         * Column resize that triggers a reorder is not a good UX and we prevent this\n         * using the following state variables: `resizing` and `preventReorder` which\n         * are set during the column's click (onClickSortColumn), pointerup\n         * (onColumnTitleMouseUp) and onStartResize events.\n         */\n        this.preventReorder = false;\n\n        this.creates = this.props.archInfo.creates.length\n            ? this.props.archInfo.creates\n            : [{ type: \"create\", string: _t(\"Add a line\") }];\n\n        this.cellToFocus = null;\n        this.activeRowId = null;\n        onMounted(async () => {\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 activeElement.\n            await Promise.resolve();\n            this.activeElement = this.uiService.activeElement;\n        });\n        onWillPatch(() => {\n            const activeRow = document.activeElement.closest(\".o_data_row.o_selected_row\");\n            this.activeRowId = activeRow ? activeRow.dataset.id : null;\n        });\n        this.optionalActiveFields = this.props.optionalActiveFields || {};\n        this.allColumns = [];\n        this.columns = [];\n        this.editedRecord = null;\n        onWillRender(() => {\n            this.editedRecord = this.props.list.editedRecord;\n            this.allColumns = this.processAllColumn(this.props.archInfo.columns, this.props.list);\n            Object.assign(this.optionalActiveFields, this.computeOptionalActiveFields());\n            this.debugOpenView = exprToBoolean(browser.localStorage.getItem(this.keyDebugOpenView));\n            this.columns = this.getActiveColumns(this.props.list);\n            this.withHandleColumn = this.columns.some((col) => col.widget === \"handle\");\n        });\n        let dataRowId;\n        this.rootRef = useRef(\"root\");\n        this.resequencePromise = Promise.resolve();\n        useSortable({\n            enable: () => this.canResequenceRows,\n            // Params\n            ref: this.rootRef,\n            elements: \".o_row_draggable\",\n            handle: \".o_handle_cell\",\n            cursor: \"grabbing\",\n            placeholderClasses: [\"d-table-row\"],\n            // Hooks\n            onDragStart: (params) => {\n                const { element } = params;\n                dataRowId = element.dataset.id;\n                return this.sortStart(params);\n            },\n            onDragEnd: (params) => this.sortStop(params),\n            onDrop: (params) => this.sortDrop(dataRowId, params),\n        });\n\n        if (this.env.searchModel) {\n            useBus(this.env.searchModel, \"focus-view\", () => {\n                if (this.props.list.model.useSampleModel) {\n                    return;\n                }\n\n                const nextTh = this.tableRef.el.querySelector(\"thead th\");\n                const toFocus = getElementToFocus(nextTh);\n                this.focus(toFocus);\n                this.tableRef.el.querySelector(\"tbody\").classList.add(\"o_keyboard_navigation\");\n            });\n        }\n\n        useBus(this.props.list.model.bus, \"FIELD_IS_DIRTY\", (ev) => (this.lastIsDirty = ev.detail));\n\n        useBounceButton(this.rootRef, () => {\n            return this.showNoContentHelper;\n        });\n\n        let isSmall = this.uiService.isSmall;\n        useBus(this.uiService.bus, \"resize\", () => {\n            if (isSmall !== this.uiService.isSmall) {\n                isSmall = this.uiService.isSmall;\n                this.render();\n            }\n        });\n\n        this.columnWidths = useMagicColumnWidths(this.tableRef, () => {\n            return {\n                columns: this.columns,\n                isEmpty: !this.props.list.records.length || this.props.list.model.useSampleModel,\n                hasSelectors: this.hasSelectors,\n                hasOpenFormViewColumn: this.hasOpenFormViewColumn,\n                hasActionsColumn: this.hasActionsColumn,\n            };\n        });\n\n        useExternalListener(window, \"keydown\", (ev) => {\n            this.shiftKeyMode = ev.shiftKey;\n        });\n        useExternalListener(window, \"keyup\", (ev) => {\n            this.shiftKeyMode = ev.shiftKey;\n            const hotkey = getActiveHotkey(ev);\n            if (hotkey === \"shift\") {\n                this.shiftKeyedRecord = undefined;\n            }\n        });\n        useExternalListener(window, \"blur\", (ev) => {\n            this.shiftKeyMode = false;\n        });\n        onPatched(async () => {\n            // HACK: we need to wait for the next tick to be sure that the Field components are patched.\n            // OWL don't wait the patch for the children components if the children trigger a patch by himself.\n            await Promise.resolve();\n\n            if (this.activeElement !== this.uiService.activeElement) {\n                return;\n            }\n            if (this.editedRecord && this.activeRowId !== this.editedRecord.id) {\n                if (this.cellToFocus && this.cellToFocus.record === this.editedRecord) {\n                    const column = this.cellToFocus.column;\n                    const forward = this.cellToFocus.forward;\n                    this.focusCell(column, forward);\n                } else if (this.lastEditedCell) {\n                    this.focusCell(this.lastEditedCell.column, true);\n                } else {\n                    this.focusCell(this.columns[0]);\n                }\n            }\n            this.cellToFocus = null;\n            this.lastEditedCell = null;\n        });\n        this.isRTL = localization.direction === \"rtl\";\n    }\n\n    displaySaveNotification() {\n        this.notificationService.add(_t(\"Please save your changes first\"), {\n            type: \"danger\",\n        });\n    }\n\n    getActiveColumns(list) {\n        return this.allColumns.filter((col) => {\n            if (list.isGrouped && col.widget === \"handle\") {\n                return false; // no handle column if the list is grouped\n            }\n            if (col.optional && !this.optionalActiveFields[col.name]) {\n                return false;\n            }\n            if (this.evalColumnInvisible(col.column_invisible)) {\n                return false;\n            }\n            return true;\n        });\n    }\n\n    get hasSelectors() {\n        return this.props.allowSelectors && !this.env.isSmall;\n    }\n\n    get hasOpenFormViewColumn() {\n        return this.props.hasOpenFormViewButton || this.debugOpenView;\n    }\n\n    get hasOptionalOpenFormViewColumn() {\n        return this.props.editable && this.env.debug && !this.props.hasOpenFormViewButton;\n    }\n\n    get hasActionsColumn() {\n        return !!(\n            this.displayOptionalFields ||\n            this.activeActions.onDelete ||\n            this.hasOptionalOpenFormViewColumn\n        );\n    }\n\n    add(params) {\n        if (this.canCreate) {\n            this.props.onAdd(params);\n        }\n    }\n\n    async addInGroup(group) {\n        const left = await this.props.list.leaveEditMode({ canAbandon: false });\n        if (left) {\n            group.addNewRecord({}, this.props.editable === \"top\");\n        }\n    }\n\n    processAllColumn(allColumns, list) {\n        return allColumns.flatMap((column) => {\n            if (column.type === \"field\" && list.fields[column.name].type === \"properties\") {\n                return this.getPropertyFieldColumns(column, list);\n            } else {\n                return [column];\n            }\n        });\n    }\n\n    getPropertyFieldColumns(column, list) {\n        return Object.values(list.fields)\n            .filter(\n                (field) =>\n                    list.activeFields[field.name] &&\n                    field.relatedPropertyField &&\n                    field.relatedPropertyField.name === column.name &&\n                    field.type !== \"separator\"\n            )\n            .map((propertyField) => {\n                const activeField = list.activeFields[propertyField.name];\n                return {\n                    ...getPropertyFieldInfo(propertyField),\n                    relatedPropertyField: activeField.relatedPropertyField,\n                    id: `${column.id}_${propertyField.name}`,\n                    column_invisible: combineModifiers(\n                        propertyField.column_invisible,\n                        column.column_invisible,\n                        \"OR\"\n                    ),\n                    classNames: column.classNames,\n                    optional: \"hide\",\n                    type: \"field\",\n                    hasLabel: true,\n                    label: propertyField.string,\n                    sortable: false,\n                    attrs: [\"integer\", \"float\"].includes(propertyField.type)\n                        ? { sum: propertyField.string }\n                        : {},\n                };\n            });\n    }\n\n    getFieldProps(record, column) {\n        return {\n            readonly: this.isCellReadonly(column, record) || this.isRecordReadonly(record),\n        };\n    }\n\n    get activeActions() {\n        return this.props.activeActions || {};\n    }\n\n    get canResequenceRows() {\n        if (!this.props.list.canResequence()) {\n            return false;\n        }\n        const { handleField, orderBy } = this.props.list;\n        return !orderBy.length || (orderBy.length && orderBy[0].name === handleField);\n    }\n\n    get fields() {\n        return this.props.list.fields;\n    }\n\n    get nbCols() {\n        let nbCols = this.columns.length;\n        if (this.hasSelectors) {\n            nbCols++;\n        }\n        if (this.hasActionsColumn) {\n            nbCols++;\n        }\n        if (this.hasOpenFormViewColumn) {\n            nbCols++;\n        }\n        return nbCols;\n    }\n\n    canUseFormatter(column, record) {\n        if (column.widget) {\n            return false;\n        }\n        if (record.isInEdition && (record.model.multiEdit || this.isInlineEditable(record))) {\n            // in a x2many non editable list, a record is in edition when it is opened in a dialog,\n            // but in the list we want it to still be displayed in readonly.\n            return false;\n        }\n        return true;\n    }\n\n    isRecordReadonly(record) {\n        if (record.isNew) {\n            return false;\n        }\n        if (this.props.activeActions?.edit === false) {\n            return true;\n        }\n        if (record.isInEdition && !this.isInlineEditable(record) && !record.model.multiEdit) {\n            // in a x2many non editable list, a record is in edition when it is opened in a dialog,\n            // but in the list we want it to still be displayed in readonly.\n            return true;\n        }\n        return false;\n    }\n\n    focusCell(column, forward = true) {\n        const index = column\n            ? this.columns.findIndex((col) => col.id === column.id && col.name === column.name)\n            : -1;\n        let columns;\n        if (index === -1 && !forward) {\n            columns = this.columns.slice(0).reverse();\n        } else {\n            columns = [\n                ...this.columns.slice(index, this.columns.length),\n                ...this.columns.slice(0, index),\n            ];\n        }\n        for (const column of columns) {\n            if (column.type !== \"field\") {\n                continue;\n            }\n            // in findNextFocusableOnRow test is done by using classList\n            // refactor\n            if (!this.isCellReadonly(column, this.editedRecord)) {\n                const cell = this.tableRef.el.querySelector(\n                    `.o_selected_row td[name='${column.name}']`\n                );\n                if (cell) {\n                    const toFocus = getElementToFocus(cell);\n                    if (cell !== toFocus) {\n                        this.focus(toFocus);\n                        this.lastEditedCell = { column, record: this.editedRecord };\n                        break;\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * @param {HTMLOrSVGElement} el\n     */\n    focus(el) {\n        if (!el) {\n            return;\n        }\n        el.focus();\n        if (\n            [\"text\", \"search\", \"url\", \"tel\", \"password\", \"textarea\"].includes(el.type) &&\n            el.selectionStart === el.selectionEnd\n        ) {\n            el.selectionStart = 0;\n            el.selectionEnd = el.value.length;\n        }\n    }\n\n    editGroupRecord(group) {\n        const { resId, resModel } = group.record;\n        this.env.services.action.doAction({\n            context: {\n                create: false,\n            },\n            res_model: resModel,\n            res_id: resId,\n            type: \"ir.actions.act_window\",\n            views: [[false, \"form\"]],\n            flags: { mode: \"edit\" },\n        });\n    }\n\n    createViewKey() {\n        let keyParts = {\n            fields: this.props.list.fieldNames, // FIXME: use something else?\n            model: this.props.list.resModel,\n            viewMode: \"list\",\n            viewId: this.env.config.viewId,\n        };\n\n        if (this.props.nestedKeyOptionalFieldsData) {\n            keyParts = Object.assign(keyParts, {\n                model: this.props.nestedKeyOptionalFieldsData.model,\n                viewMode: this.props.nestedKeyOptionalFieldsData.viewMode,\n                relationalField: this.props.nestedKeyOptionalFieldsData.field,\n                subViewType: \"list\",\n            });\n        }\n\n        const parts = [\"model\", \"viewMode\", \"viewId\", \"relationalField\", \"subViewType\"];\n        const viewIdentifier = [];\n        parts.forEach((partName) => {\n            if (partName in keyParts) {\n                viewIdentifier.push(keyParts[partName]);\n            }\n        });\n        keyParts.fields\n            .sort((left, right) => (left < right ? -1 : 1))\n            .forEach((fieldName) => {\n                return viewIdentifier.push(fieldName);\n            });\n        return viewIdentifier.join(\",\");\n    }\n\n    get optionalFieldGroups() {\n        const propertyGroups = {};\n        const optionalFields = [];\n        const optionalColumns = this.allColumns.filter(\n            (col) => col.optional && !this.evalColumnInvisible(col.column_invisible)\n        );\n        for (const col of optionalColumns) {\n            const optionalField = {\n                label: col.label,\n                name: col.name,\n                value: this.optionalActiveFields[col.name],\n            };\n            if (!col.relatedPropertyField) {\n                optionalFields.push(optionalField);\n            } else {\n                const { displayName, id } = col.relatedPropertyField;\n                if (propertyGroups[id]) {\n                    propertyGroups[id].optionalFields.push(optionalField);\n                } else {\n                    propertyGroups[id] = { id, displayName, optionalFields: [optionalField] };\n                }\n            }\n        }\n        if (optionalFields.length) {\n            return [{ optionalFields }, ...Object.values(propertyGroups)];\n        }\n        return Object.values(propertyGroups);\n    }\n\n    get hasOptionalFields() {\n        return this.allColumns.some(\n            (col) => col.optional && !this.evalColumnInvisible(col.column_invisible)\n        );\n    }\n\n    get displayOptionalFields() {\n        return this.hasOptionalFields;\n    }\n\n    nbRecordsInGroup(group) {\n        if (group.isFolded) {\n            return 0;\n        } else if (group.list.isGrouped) {\n            let count = 0;\n            for (const gr of group.list.groups) {\n                count += this.nbRecordsInGroup(gr);\n            }\n            return count;\n        } else {\n            return group.list.records.length;\n        }\n    }\n    get selectAll() {\n        const list = this.props.list;\n        const nbDisplayedRecords = list.records.length;\n        if (list.isDomainSelected) {\n            return true;\n        } else {\n            return nbDisplayedRecords > 0 && list.selection.length === nbDisplayedRecords;\n        }\n    }\n\n    get aggregates() {\n        let values;\n        if (this.props.list.selection && this.props.list.selection.length) {\n            values = this.props.list.selection.map((r) => r.data);\n        } else if (this.props.list.isGrouped) {\n            values = this.props.list.groups.map((g) => g.aggregates);\n        } else {\n            values = this.props.list.records.map((r) => r.data);\n        }\n        const aggregates = {};\n        for (const column of this.allColumns) {\n            if (column.type !== \"field\") {\n                continue;\n            }\n            const fieldName = column.name;\n            if (fieldName in this.optionalActiveFields && !this.optionalActiveFields[fieldName]) {\n                continue;\n            }\n            const field = this.fields[fieldName];\n            const fieldValues = values.map((v) => v[fieldName]).filter((v) => v || v === 0);\n            if (!fieldValues.length) {\n                continue;\n            }\n            const type = field.type;\n            if (type !== \"integer\" && type !== \"float\" && type !== \"monetary\") {\n                continue;\n            }\n            const { attrs, widget } = column;\n            const func =\n                (attrs.sum && \"sum\") ||\n                (attrs.avg && \"avg\") ||\n                (attrs.max && \"max\") ||\n                (attrs.min && \"min\");\n            let currencyId;\n            if (type === \"monetary\" || widget === \"monetary\") {\n                const currencyField =\n                    column.options.currency_field ||\n                    this.fields[fieldName].currency_field ||\n                    \"currency_id\";\n                if (!(currencyField in this.props.list.activeFields)) {\n                    aggregates[fieldName] = {\n                        help: _t(\"No currency provided\"),\n                        value: \"\u2014\",\n                    };\n                    continue;\n                }\n                currencyId = values[0][currencyField] && values[0][currencyField][0];\n                if (currencyId && func) {\n                    const sameCurrency = values.every(\n                        (value) => currencyId === value[currencyField][0]\n                    );\n                    if (!sameCurrency) {\n                        aggregates[fieldName] = {\n                            help: _t(\"Different currencies cannot be aggregated\"),\n                            value: \"\u2014\",\n                        };\n                        continue;\n                    }\n                }\n            }\n            if (func) {\n                let aggregateValue = 0;\n                if (func === \"max\") {\n                    aggregateValue = Math.max(-Infinity, ...fieldValues);\n                } else if (func === \"min\") {\n                    aggregateValue = Math.min(Infinity, ...fieldValues);\n                } else if (func === \"avg\") {\n                    aggregateValue =\n                        fieldValues.reduce((acc, val) => acc + val) / fieldValues.length;\n                } else if (func === \"sum\") {\n                    aggregateValue = fieldValues.reduce((acc, val) => acc + val);\n                }\n\n                const formatter = formatters.get(widget, false) || formatters.get(type, false);\n                const formatOptions = {\n                    digits: attrs.digits ? JSON.parse(attrs.digits) : undefined,\n                    escape: true,\n                };\n                if (currencyId) {\n                    formatOptions.currencyId = currencyId;\n                }\n                aggregates[fieldName] = {\n                    help: attrs[func],\n                    value: formatter ? formatter(aggregateValue, formatOptions) : aggregateValue,\n                };\n            }\n        }\n        return aggregates;\n    }\n\n    formatAggregateValue(group, column) {\n        const { widget, attrs } = column;\n        const field = this.props.list.fields[column.name];\n        const aggregateValue = group.aggregates[column.name];\n        if (!(column.name in group.aggregates)) {\n            return \"\";\n        }\n        const formatter = formatters.get(widget, false) || formatters.get(field.type, false);\n        const formatOptions = {\n            digits: attrs.digits ? JSON.parse(attrs.digits) : field.digits,\n            escape: true,\n        };\n        return formatter ? formatter(aggregateValue, formatOptions) : aggregateValue;\n    }\n\n    getGroupLevel(group) {\n        return this.props.list.groupBy.length - group.list.groupBy.length - 1;\n    }\n\n    getColumnClass(column) {\n        const classNames = [\"align-middle\"];\n        if (this.isSortable(column)) {\n            classNames.push(\"o_column_sortable\", \"position-relative\", \"cursor-pointer\");\n        } else {\n            classNames.push(\"cursor-default\");\n        }\n        const orderBy = this.props.list.orderBy;\n        if (\n            orderBy.length &&\n            column.widget !== \"handle\" &&\n            orderBy[0].name === column.name &&\n            column.hasLabel\n        ) {\n            classNames.push(\"table-active\");\n        }\n        if (this.isNumericColumn(column)) {\n            classNames.push(\"o_list_number_th\");\n        }\n        if (column.type === \"button_group\") {\n            classNames.push(\"o_list_button\");\n        }\n        if (column.widget) {\n            classNames.push(`o_${column.widget}_cell`);\n        }\n\n        return classNames.join(\" \");\n    }\n\n    getColumns(record) {\n        return this.columns;\n    }\n\n    isNumericColumn(column) {\n        const { type } = this.fields[column.name];\n        return [\"float\", \"integer\", \"monetary\"].includes(type);\n    }\n\n    isSortable(column) {\n        const { hasLabel, name, options } = column;\n        const { sortable } = this.fields[name];\n        return (sortable || options.allow_order) && hasLabel;\n    }\n\n    getSortableIconClass(column) {\n        const { orderBy } = this.props.list;\n        const classNames = this.isSortable(column) ? [\"fa\", \"fa-lg\"] : [\"d-none\"];\n        if (orderBy.length && orderBy[0].name === column.name) {\n            classNames.push(orderBy[0].asc ? \"fa-angle-up\" : \"fa-angle-down\");\n        } else {\n            classNames.push(\"fa-angle-down\", \"opacity-0\", \"opacity-100-hover\");\n        }\n\n        return classNames.join(\" \");\n    }\n\n    /**\n     * Returns the classnames to apply to the row representing the given record.\n     * @param {Record} record\n     * @returns {string}\n     */\n    getRowClass(record) {\n        // classnames coming from decorations\n        const classNames = this.props.archInfo.decorations\n            .filter((decoration) =>\n                evaluateBooleanExpr(decoration.condition, record.evalContextWithVirtualIds)\n            )\n            .map((decoration) => decoration.class);\n        if (record.selected) {\n            classNames.push(\"table-info\");\n        }\n        // \"o_selected_row\" classname for the potential row in edition\n        if (record.isInEdition) {\n            classNames.push(\"o_selected_row\");\n        }\n        if (record.selected) {\n            classNames.push(\"o_data_row_selected\");\n        }\n        if (this.canResequenceRows) {\n            classNames.push(\"o_row_draggable\");\n        }\n        return classNames.join(\" \");\n    }\n\n    getCellClass(column, record) {\n        if (column.relatedPropertyField && !(column.name in record.data)) {\n            return \"\";\n        }\n\n        if (!this.cellClassByColumn[column.id]) {\n            const classNames = [\"o_data_cell\"];\n            if (column.type === \"button_group\") {\n                classNames.push(\"o_list_button\");\n            } else if (column.type === \"field\") {\n                classNames.push(\"o_field_cell\");\n                if (column.attrs && column.attrs.class && this.canUseFormatter(column, record)) {\n                    classNames.push(column.attrs.class);\n                }\n                const typeClass = FIELD_CLASSES[this.fields[column.name].type];\n                if (typeClass) {\n                    classNames.push(typeClass);\n                }\n                if (column.widget) {\n                    classNames.push(`o_${column.widget}_cell`);\n                }\n            }\n            this.cellClassByColumn[column.id] = classNames;\n        }\n        const classNames = [...this.cellClassByColumn[column.id]];\n        if (column.type === \"field\") {\n            if (evaluateBooleanExpr(column.required, record.evalContextWithVirtualIds)) {\n                classNames.push(\"o_required_modifier\");\n            }\n            if (record.isFieldInvalid(column.name)) {\n                classNames.push(\"o_invalid_cell\");\n            }\n            if (this.isCellReadonly(column, record)) {\n                classNames.push(\"o_readonly_modifier\");\n            }\n            if (this.canUseFormatter(column, record)) {\n                // generate field decorations classNames (only if field-specific decorations\n                // have been defined in an attribute, e.g. decoration-danger=\"other_field = 5\")\n                // only handle the text-decoration.\n                const { decorations } = column;\n                for (const decoName in decorations) {\n                    if (\n                        evaluateBooleanExpr(decorations[decoName], record.evalContextWithVirtualIds)\n                    ) {\n                        classNames.push(getClassNameFromDecoration(decoName));\n                    }\n                }\n            }\n            if (\n                record.isInEdition &&\n                this.editedRecord &&\n                this.isCellReadonly(column, this.editedRecord)\n            ) {\n                classNames.push(\"text-muted\");\n            } else {\n                classNames.push(\"cursor-pointer\");\n            }\n        }\n        return classNames.join(\" \");\n    }\n\n    isCellReadonly(column, record) {\n        return !!(\n            this.isRecordReadonly(record) ||\n            (column.relatedPropertyField && record.selected && record.model.multiEdit) ||\n            evaluateBooleanExpr(column.readonly, record.evalContextWithVirtualIds)\n        );\n    }\n\n    getCellTitle(column, record) {\n        // Because we freeze the column sizes, it may happen that we have to shorten field values.\n        // In order for the user to have access to the complete value in those situations, we put\n        // the value as title of the cells.\n        if ([\"many2one\", \"reference\", \"char\"].includes(this.fields[column.name].type)) {\n            return this.getFormattedValue(column, record);\n        }\n    }\n\n    getFieldClass(column) {\n        return column.attrs && column.attrs.class;\n    }\n\n    getFormattedValue(column, record) {\n        const fieldName = column.name;\n        if (column.options.enable_formatting === false) {\n            return record.data[fieldName];\n        }\n        return getFormattedValue(record, fieldName, column);\n    }\n\n    evalInvisible(invisible, record) {\n        return evaluateBooleanExpr(invisible, record.evalContextWithVirtualIds);\n    }\n\n    evalColumnInvisible(columnInvisible) {\n        return evaluateBooleanExpr(columnInvisible, this.props.list.evalContext);\n    }\n\n    getGroupDisplayName(group) {\n        if (group.groupByField.type === \"boolean\") {\n            return group.value === undefined ? _t(\"None\") : group.value ? _t(\"Yes\") : _t(\"No\");\n        } else {\n            return group.value === undefined || group.value === false\n                ? _t(\"None\")\n                : group.displayName;\n        }\n    }\n\n    get canCreate() {\n        return \"link\" in this.activeActions ? this.activeActions.link : this.activeActions.create;\n    }\n\n    get isX2Many() {\n        return this.activeActions.type !== \"view\";\n    }\n\n    get getEmptyRowIds() {\n        let nbEmptyRow = Math.max(0, 4 - this.props.list.records.length);\n        if (nbEmptyRow > 0 && this.displayRowCreates) {\n            nbEmptyRow -= 1;\n        }\n        return Array.from(Array(nbEmptyRow).keys());\n    }\n\n    get displayRowCreates() {\n        return this.isX2Many && this.canCreate;\n    }\n\n    // Group headers logic:\n    // if there are aggregates, the first th spans until the first\n    // aggregate column then all cells between aggregates are rendered\n    // a single cell is rendered after the last aggregated column to render the\n    // pager (with adequate colspan)\n    // ex:\n    // TH TH TH TH TH AGG AGG TH AGG AGG TH TH TH\n    // 0  1  2  3  4   5   6   7  8   9  10 11 12\n    // [    TH 5    ][TH][TH][TH][TH][TH][ TH 3 ]\n    // [ group name ][ aggregate cells  ][ pager]\n    // TODO: move this somewhere, compute this only once (same result for each groups actually) ?\n    getFirstAggregateIndex(group) {\n        return this.columns.findIndex((col) => col.name in group.aggregates);\n    }\n    getLastAggregateIndex(group) {\n        const reversedColumns = [...this.columns].reverse(); // reverse is destructive\n        const index = reversedColumns.findIndex((col) => col.name in group.aggregates);\n        return index > -1 ? this.columns.length - index - 1 : -1;\n    }\n    getAggregateColumns(group) {\n        const firstIndex = this.getFirstAggregateIndex(group);\n        const lastIndex = this.getLastAggregateIndex(group);\n        return this.columns.slice(firstIndex, lastIndex + 1);\n    }\n    getGroupNameCellColSpan(group) {\n        // if there are aggregates, the first th spans until the first\n        // aggregate column then all cells between aggregates are rendered\n        const firstAggregateIndex = this.getFirstAggregateIndex(group);\n        let colspan;\n        if (firstAggregateIndex > -1) {\n            colspan = firstAggregateIndex;\n        } else {\n            colspan = Math.max(1, this.columns.length - DEFAULT_GROUP_PAGER_COLSPAN);\n            if (this.displayOptionalFields) {\n                colspan++;\n            }\n        }\n        if (this.hasSelectors) {\n            colspan++;\n        }\n        return colspan;\n    }\n\n    getGroupPagerCellColspan(group) {\n        const lastAggregateIndex = this.getLastAggregateIndex(group);\n        let colspan;\n        if (lastAggregateIndex > -1) {\n            colspan = this.columns.length - lastAggregateIndex - 1;\n            if (this.displayOptionalFields) {\n                colspan++;\n            }\n        } else {\n            colspan = this.columns.length > 1 ? DEFAULT_GROUP_PAGER_COLSPAN : 0;\n        }\n        if (this.hasOpenFormViewColumn) {\n            colspan++;\n        }\n        return colspan;\n    }\n\n    getGroupPagerProps(group) {\n        const list = group.list;\n        return {\n            offset: list.offset,\n            limit: list.limit,\n            total: list.count,\n            onUpdate: async ({ offset, limit }) => {\n                await list.load({ limit, offset });\n                this.render(true);\n            },\n            withAccessKey: false,\n        };\n    }\n\n    computeOptionalActiveFields() {\n        const localStorageValue = browser.localStorage.getItem(this.keyOptionalFields);\n        const optionalColumn = this.allColumns.filter(\n            (col) => col.type === \"field\" && col.optional\n        );\n        const optionalActiveFields = {};\n        if (localStorageValue !== null) {\n            const localStorageOptionalActiveFields = localStorageValue.split(\",\");\n            for (const col of optionalColumn) {\n                optionalActiveFields[col.name] = localStorageOptionalActiveFields.includes(\n                    col.name\n                );\n            }\n        } else {\n            for (const col of optionalColumn) {\n                optionalActiveFields[col.name] = col.optional === \"show\";\n            }\n        }\n        return optionalActiveFields;\n    }\n\n    onClickSortColumn(column) {\n        if (this.preventReorder) {\n            this.preventReorder = false;\n            return;\n        }\n        if (this.editedRecord || this.props.list.model.useSampleModel) {\n            return;\n        }\n        const fieldName = column.name;\n        const list = this.props.list;\n        if (this.isSortable(column)) {\n            list.sortBy(fieldName);\n        }\n    }\n\n    onButtonCellClicked(record, column, ev) {\n        if (!ev.target.closest(\"button\")) {\n            this.onCellClicked(record, column, ev);\n        }\n    }\n\n    /**\n     * @param {Object} record\n     * @param {Object} column\n     * @param {PointerEvent} ev\n     */\n    async onCellClicked(record, column, ev) {\n        if (ev.target.special_click) {\n            return;\n        }\n        const recordAfterResequence = async () => {\n            const recordIndex = this.props.list.records.indexOf(record);\n            await this.resequencePromise;\n            // row might have changed record after resequence\n            record = this.props.list.records[recordIndex] || record;\n        };\n\n        if ((this.props.list.model.multiEdit && record.selected) || this.isInlineEditable(record)) {\n            if (record.isInEdition && this.editedRecord === record) {\n                const cell = this.tableRef.el.querySelector(\n                    `.o_selected_row td[name='${column.name}']`\n                );\n                if (cell && containsActiveElement(cell)) {\n                    this.lastEditedCell = { column, record };\n                    // Cell is already focused.\n                    return;\n                }\n                this.focusCell(column);\n                this.cellToFocus = null;\n            } else {\n                await recordAfterResequence();\n                await this.props.list.enterEditMode(record);\n                this.cellToFocus = { column, record };\n                if (\n                    column.type === \"field\" &&\n                    record.fields[column.name].type === \"boolean\" &&\n                    (!column.widget || column.widget === \"boolean\")\n                ) {\n                    if (\n                        !this.isCellReadonly(column, record) &&\n                        !this.evalInvisible(column.invisible, record)\n                    ) {\n                        await record.update({ [column.name]: !record.data[column.name] });\n                    }\n                }\n            }\n        } else if (this.editedRecord && this.editedRecord !== record) {\n            this.props.list.leaveEditMode();\n        } else if (!this.props.archInfo.noOpen) {\n            this.props.openRecord(record);\n        }\n    }\n\n    onRemoveCellClicked(record, ev) {\n        const element = ev.target.closest(\".o_list_record_remove\");\n        if (element.dataset.clicked) {\n            return;\n        }\n        element.dataset.clicked = true;\n\n        this.onDeleteRecord(record, ev);\n    }\n\n    async onDeleteRecord(record) {\n        this.keepColumnWidths = true;\n        if (this.editedRecord && this.editedRecord !== record) {\n            const left = await this.props.list.leaveEditMode();\n            if (!left) {\n                return;\n            }\n        }\n        if (this.activeActions.onDelete) {\n            this.activeActions.onDelete(record);\n        }\n    }\n\n    /**\n     * @param {HTMLTableCellElement} cell\n     * @param {boolean} cellIsInGroupRow\n     * @param {\"up\"|\"down\"|\"left\"|\"right\"} direction\n     */\n    findFocusFutureCell(cell, cellIsInGroupRow, direction) {\n        const row = cell.parentElement;\n        const children = [...row.children];\n        const index = children.indexOf(cell);\n        let futureCell;\n        switch (direction) {\n            case \"up\": {\n                let futureRow = row.previousElementSibling;\n                futureRow = futureRow || row.parentElement.previousElementSibling?.lastElementChild;\n\n                if (futureRow) {\n                    const addCell = [...futureRow.children].find((c) =>\n                        c.classList.contains(\"o_group_field_row_add\")\n                    );\n                    const nextIsGroup = futureRow.classList.contains(\"o_group_header\");\n                    const rowTypeSwitched = cellIsInGroupRow !== nextIsGroup;\n                    let defaultIndex = 0;\n                    if (cellIsInGroupRow) {\n                        defaultIndex = this.hasSelectors ? 1 : 0;\n                    }\n                    futureCell =\n                        addCell ||\n                        (futureRow && futureRow.children[rowTypeSwitched ? defaultIndex : index]);\n                }\n                break;\n            }\n            case \"down\": {\n                let futureRow = row.nextElementSibling;\n                futureRow = futureRow || row.parentElement.nextElementSibling?.firstElementChild;\n                if (futureRow) {\n                    const addCell = [...futureRow.children].find((c) =>\n                        c.classList.contains(\"o_group_field_row_add\")\n                    );\n                    const nextIsGroup = futureRow.classList.contains(\"o_group_header\");\n                    const rowTypeSwitched = cellIsInGroupRow !== nextIsGroup;\n                    let defaultIndex = 0;\n                    if (cellIsInGroupRow) {\n                        defaultIndex = this.hasSelectors ? 1 : 0;\n                    }\n                    futureCell =\n                        addCell ||\n                        (futureRow && futureRow.children[rowTypeSwitched ? defaultIndex : index]);\n                }\n                break;\n            }\n            case \"left\": {\n                futureCell = children[index - 1];\n                break;\n            }\n            case \"right\": {\n                futureCell = children[index + 1];\n                break;\n            }\n        }\n        return futureCell && getElementToFocus(futureCell);\n    }\n\n    isInlineEditable(record) {\n        // /!\\ the keyboard navigation works under the hypothesis that all or\n        // none records are editable.\n        return !!this.props.editable;\n    }\n\n    /**\n     * @param {KeyboardEvent} ev\n     * @param { import('@web/model/relational_model/group').Group | null } group\n     * @param { import('@web/model/relational_model/record').Record | null } record\n     */\n    onCellKeydown(ev, group = null, record = null) {\n        if (this.props.list.model.useSampleModel) {\n            return;\n        }\n\n        const hotkey = getActiveHotkey(ev);\n\n        if (ev.target.tagName === \"TEXTAREA\" && hotkey === \"enter\") {\n            return;\n        }\n\n        const closestCell = ev.target.closest(\"td, th\");\n\n        if (this.toggleFocusInsideCell(hotkey, closestCell)) {\n            return;\n        }\n\n        const handled = this.editedRecord\n            ? this.onCellKeydownEditMode(hotkey, closestCell, group, record)\n            : this.onCellKeydownReadOnlyMode(hotkey, closestCell, group, record); // record is supposed to be not null here\n\n        if (handled) {\n            this.lastCreatingAction = false;\n            this.tableRef.el.querySelector(\"tbody\").classList.add(\"o_keyboard_navigation\");\n            ev.preventDefault();\n            ev.stopPropagation();\n        }\n    }\n\n    findNextFocusableOnRow(row, cell) {\n        const children = [...row.children];\n        const index = children.indexOf(cell);\n        const nextCells = children.slice(index + 1);\n        for (const c of nextCells) {\n            if (!c.classList.contains(\"o_data_cell\")) {\n                continue;\n            }\n            if (\n                c.firstElementChild &&\n                c.firstElementChild.classList.contains(\"o_readonly_modifier\")\n            ) {\n                continue;\n            }\n            const toFocus = getElementToFocus(c, 0);\n            if (toFocus !== c) {\n                return toFocus;\n            }\n        }\n\n        return null;\n    }\n\n    findPreviousFocusableOnRow(row, cell) {\n        const children = [...row.children];\n        const index = children.indexOf(cell);\n        const previousCells = children.slice(0, index);\n        for (const c of previousCells.reverse()) {\n            if (!c.classList.contains(\"o_data_cell\")) {\n                continue;\n            }\n            if (\n                c.firstElementChild &&\n                c.firstElementChild.classList.contains(\"o_readonly_modifier\")\n            ) {\n                continue;\n            }\n            const toFocus = getElementToFocus(c, -1);\n            if (toFocus !== c) {\n                return toFocus;\n            }\n        }\n\n        return null;\n    }\n\n    expandCheckboxes(record, direction) {\n        const { records } = this.props.list;\n        if (!record && direction === \"down\") {\n            const defaultRecord = records[0];\n            this.shiftKeyedRecord = defaultRecord;\n            defaultRecord.toggleSelection(true);\n            return true;\n        }\n        const recordIndex = records.indexOf(record);\n        const shiftKeyedRecordIndex = records.indexOf(this.shiftKeyedRecord);\n        let nextRecord;\n        let isExpanding;\n        switch (direction) {\n            case \"up\":\n                if (recordIndex <= 0) {\n                    return false;\n                }\n                nextRecord = records[recordIndex - 1];\n                isExpanding = shiftKeyedRecordIndex > recordIndex - 1;\n                break;\n            case \"down\":\n                if (recordIndex === records.length - 1) {\n                    return false;\n                }\n                nextRecord = records[recordIndex + 1];\n                isExpanding = shiftKeyedRecordIndex < recordIndex + 1;\n                break;\n        }\n\n        if (isExpanding) {\n            record.toggleSelection(true);\n            nextRecord.toggleSelection(true);\n        } else {\n            record.toggleSelection(false);\n        }\n\n        return true;\n    }\n\n    applyCellKeydownMultiEditMode(hotkey, cell, group, record) {\n        const { list } = this.props;\n        const row = cell.parentElement;\n        let toFocus, futureRecord;\n        const index = list.selection.indexOf(record);\n        if (this.lastIsDirty && [\"tab\", \"shift+tab\", \"enter\"].includes(hotkey)) {\n            list.leaveEditMode();\n            return true;\n        }\n\n        if (this.applyCellKeydownEditModeStayOnRow(hotkey, cell, group, record)) {\n            return true;\n        }\n\n        switch (hotkey) {\n            case \"tab\":\n                futureRecord = list.selection[index + 1] || list.selection[0];\n                if (record === futureRecord) {\n                    // Refocus first cell of same record\n                    toFocus = this.findNextFocusableOnRow(row, cell);\n                    this.focus(toFocus);\n                    return true;\n                }\n                break;\n\n            case \"shift+tab\":\n                futureRecord =\n                    list.selection[index - 1] || list.selection[list.selection.length - 1];\n                if (record === futureRecord) {\n                    // Refocus last cell of same record\n                    toFocus = this.findPreviousFocusableOnRow(row, cell);\n                    this.focus(toFocus);\n                    return true;\n                }\n                this.cellToFocus = { forward: false, record: futureRecord };\n                break;\n\n            case \"enter\":\n                if (list.selection.length === 1) {\n                    list.leaveEditMode();\n                    return true;\n                }\n                futureRecord = list.selection[index + 1] || list.selection[0];\n                break;\n        }\n\n        if (futureRecord) {\n            list.enterEditMode(futureRecord);\n            return true;\n        }\n        return false;\n    }\n\n    applyCellKeydownEditModeGroup(hotkey, _cell, group, record) {\n        const { editable } = this.props;\n        const groupIndex = group.list.records.indexOf(record);\n        const isLastOfGroup = groupIndex === group.list.records.length - 1;\n        const isDirty = record.dirty || this.lastIsDirty;\n        const isEnterBehavior = hotkey === \"enter\" && (!record.canBeAbandoned || isDirty);\n        const isTabBehavior = hotkey === \"tab\" && !record.canBeAbandoned && isDirty;\n        if (\n            isLastOfGroup &&\n            this.canCreate &&\n            editable === \"bottom\" &&\n            (isEnterBehavior || isTabBehavior)\n        ) {\n            this.add({ group });\n            return true;\n        }\n        return false;\n    }\n\n    applyCellKeydownEditModeStayOnRow(hotkey, cell, group, record) {\n        let toFocus;\n        const row = cell.parentElement;\n\n        switch (hotkey) {\n            case \"tab\":\n                toFocus = this.findNextFocusableOnRow(row, cell);\n                break;\n            case \"shift+tab\":\n                toFocus = this.findPreviousFocusableOnRow(row, cell);\n                break;\n        }\n\n        if (toFocus) {\n            this.focus(toFocus);\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     * @param {string} hotkey\n     * @param {HTMLTableCellElement} cell\n     * @param { import('@web/model/relational_model/group').Group | null } group\n     * @param { import('@web/model/relational_model/record').Record } record\n     * @returns {boolean} true if some behavior has been taken\n     */\n    onCellKeydownEditMode(hotkey, cell, group, record) {\n        const { cycleOnTab, list } = this.props;\n        const row = cell.parentElement;\n        const applyMultiEditBehavior = record && record.selected && list.model.multiEdit;\n        const topReCreate = this.props.editable === \"top\" && record.isNew;\n\n        if (\n            applyMultiEditBehavior &&\n            this.applyCellKeydownMultiEditMode(hotkey, cell, group, record)\n        ) {\n            return true;\n        }\n\n        if (this.applyCellKeydownEditModeStayOnRow(hotkey, cell, group, record)) {\n            return true;\n        }\n\n        if (group && this.applyCellKeydownEditModeGroup(hotkey, cell, group, record)) {\n            return true;\n        }\n\n        switch (hotkey) {\n            case \"tab\": {\n                const index = list.records.indexOf(record);\n                const lastIndex = topReCreate ? 0 : list.records.length - 1;\n                if (index === lastIndex || index === list.records.length - 1) {\n                    if (this.displayRowCreates) {\n                        if (record.isNew && !record.dirty) {\n                            list.leaveEditMode();\n                            return false;\n                        }\n                        // add a line\n                        const { context } = this.creates[0];\n                        this.add({ context });\n                    } else if (\n                        this.canCreate &&\n                        !record.canBeAbandoned &&\n                        (record.dirty || this.lastIsDirty)\n                    ) {\n                        this.add({ group });\n                    } else if (cycleOnTab) {\n                        if (record.canBeAbandoned) {\n                            list.leaveEditMode();\n                        }\n                        const futureRecord = list.records[0];\n                        if (record === futureRecord) {\n                            // Refocus first cell of same record\n                            const toFocus = this.findNextFocusableOnRow(row);\n                            this.focus(toFocus);\n                        } else {\n                            list.enterEditMode(futureRecord);\n                        }\n                    } else {\n                        return false;\n                    }\n                } else {\n                    const futureRecord = list.records[index + 1];\n                    list.enterEditMode(futureRecord);\n                }\n                break;\n            }\n            case \"shift+tab\": {\n                const index = list.records.indexOf(record);\n                if (index === 0) {\n                    if (cycleOnTab) {\n                        if (record.canBeAbandoned) {\n                            list.leaveEditMode();\n                        }\n                        const futureRecord = list.records[list.records.length - 1];\n                        if (record === futureRecord) {\n                            // Refocus first cell of same record\n                            const toFocus = this.findPreviousFocusableOnRow(row);\n                            this.focus(toFocus);\n                        } else {\n                            this.cellToFocus = { forward: false, record: futureRecord };\n                            list.enterEditMode(futureRecord);\n                        }\n                    } else {\n                        list.leaveEditMode();\n                        return false;\n                    }\n                } else {\n                    const futureRecord = list.records[index - 1];\n                    this.cellToFocus = { forward: false, record: futureRecord };\n                    list.enterEditMode(futureRecord);\n                }\n                break;\n            }\n            case \"enter\": {\n                const index = list.records.indexOf(record);\n                let futureRecord = list.records[index + 1];\n                if (topReCreate && index === 0) {\n                    futureRecord = null;\n                }\n\n                if (!futureRecord && !this.canCreate) {\n                    futureRecord = list.records[0];\n                }\n\n                if (futureRecord) {\n                    list.leaveEditMode({ validate: true }).then((canProceed) => {\n                        if (canProceed) {\n                            list.enterEditMode(futureRecord);\n                        }\n                    });\n                } else if (this.lastIsDirty || !record.canBeAbandoned || this.displayRowCreates) {\n                    this.add({ group });\n                } else {\n                    futureRecord = list.records.at(0);\n                    list.enterEditMode(futureRecord);\n                }\n                break;\n            }\n            case \"escape\": {\n                // TODO this seems bad: refactor this\n                list.leaveEditMode({ discard: true });\n                const firstAddButton = this.tableRef.el.querySelector(\n                    \".o_field_x2many_list_row_add a\"\n                );\n\n                if (firstAddButton) {\n                    this.focus(firstAddButton);\n                } else if (group && record.isNew) {\n                    const children = [...row.parentElement.children];\n                    const index = children.indexOf(row);\n                    for (let i = index + 1; i < children.length; i++) {\n                        const row = children[i];\n                        if (row.classList.contains(\"o_group_header\")) {\n                            break;\n                        }\n                        const addCell = [...row.children].find((c) =>\n                            c.classList.contains(\"o_group_field_row_add\")\n                        );\n                        if (addCell) {\n                            const toFocus = addCell.querySelector(\"a\");\n                            this.focus(toFocus);\n                            return true;\n                        }\n                    }\n                    this.focus(cell);\n                } else {\n                    this.focus(cell);\n                }\n                break;\n            }\n            default:\n                return false;\n        }\n        return true;\n    }\n\n    /**\n     * @param {string} hotkey\n     * @param {HTMLTableCellElement} cell\n     * @param { import('@web/model/relational_model/group').Group\n     *  | null\n     * } group\n     * @param { import('@web/model/relational_model/record').Record | null } record\n     * @returns {boolean} true if some behavior has been taken\n     */\n    onCellKeydownReadOnlyMode(hotkey, cell, group, record) {\n        const cellIsInGroupRow = Boolean(group && !record);\n        const applyMultiEditBehavior = record && record.selected && this.props.list.model.multiEdit;\n        let toFocus;\n        switch (hotkey) {\n            case \"arrowup\":\n                toFocus = this.findFocusFutureCell(cell, cellIsInGroupRow, \"up\");\n                if (!toFocus && this.env.searchModel) {\n                    this.env.searchModel.trigger(\"focus-search\");\n                    return true;\n                }\n                break;\n            case \"arrowdown\":\n                toFocus = this.findFocusFutureCell(cell, cellIsInGroupRow, \"down\");\n                break;\n            case \"arrowleft\":\n                if (cellIsInGroupRow && !group.isFolded) {\n                    this.toggleGroup(group);\n                    return true;\n                }\n\n                if (cell.classList.contains(\"o_field_x2many_list_row_add\")) {\n                    // to refactor\n                    const a = document.activeElement;\n                    toFocus = a.previousElementSibling;\n                } else {\n                    toFocus = this.findFocusFutureCell(cell, cellIsInGroupRow, \"left\");\n                }\n                break;\n            case \"arrowright\":\n                if (cellIsInGroupRow && group.isFolded) {\n                    this.toggleGroup(group);\n                    return true;\n                }\n\n                if (cell.classList.contains(\"o_field_x2many_list_row_add\")) {\n                    // This cell contains only <a/> elements, see template.\n                    const a = document.activeElement;\n                    toFocus = a.nextElementSibling;\n                } else {\n                    toFocus = this.findFocusFutureCell(cell, cellIsInGroupRow, \"right\");\n                }\n                break;\n            case \"tab\":\n                if (cellIsInGroupRow) {\n                    const buttons = Array.from(cell.querySelectorAll(\".o_group_buttons button\"));\n                    const currentButton = document.activeElement.closest(\"button\");\n                    const index = buttons.indexOf(currentButton);\n                    toFocus = buttons[index + 1] || currentButton;\n                }\n                break;\n            case \"shift+tab\":\n                if (cellIsInGroupRow) {\n                    const buttons = Array.from(cell.querySelectorAll(\".o_group_buttons button\"));\n                    const currentButton = document.activeElement.closest(\"button\");\n                    const index = buttons.indexOf(currentButton);\n                    toFocus = buttons[index - 1] || currentButton;\n                }\n                break;\n            case \"shift+arrowdown\": {\n                if (this.expandCheckboxes(record, \"down\")) {\n                    toFocus = this.findFocusFutureCell(cell, cellIsInGroupRow, \"down\");\n                }\n                break;\n            }\n            case \"shift+arrowup\": {\n                if (this.expandCheckboxes(record, \"up\")) {\n                    toFocus = this.findFocusFutureCell(cell, cellIsInGroupRow, \"up\");\n                }\n                break;\n            }\n            case \"shift+space\":\n                this.toggleRecordSelection(record);\n                toFocus = getElementToFocus(cell);\n                break;\n            case \"shift\":\n                this.shiftKeyedRecord = record;\n                break;\n            case \"enter\":\n                if (!group && !record) {\n                    return false;\n                }\n\n                if (cell.classList.contains(\"o_list_record_remove\")) {\n                    this.onDeleteRecord(record);\n                    return true;\n                }\n\n                if (cellIsInGroupRow) {\n                    const button = document.activeElement.closest(\"button\");\n                    if (button) {\n                        button.click();\n                    } else {\n                        this.toggleGroup(group);\n                    }\n                    return true;\n                }\n\n                if (this.isInlineEditable(record) || applyMultiEditBehavior) {\n                    const column = this.columns.find((c) => c.name === cell.getAttribute(\"name\"));\n                    this.cellToFocus = { column, record };\n                    this.props.list.enterEditMode(record);\n                    return true;\n                }\n\n                if (!this.props.archInfo.noOpen) {\n                    this.props.openRecord(record);\n                    return true;\n                }\n                break;\n            default:\n                // Return with no effect (no stop or prevent default...)\n                return false;\n        }\n\n        if (toFocus) {\n            this.focus(toFocus);\n            return true;\n        }\n\n        return false;\n    }\n\n    saveOptionalActiveFields() {\n        browser.localStorage.setItem(\n            this.keyOptionalFields,\n            Object.keys(this.optionalActiveFields).filter(\n                (fieldName) => this.optionalActiveFields[fieldName]\n            )\n        );\n    }\n\n    get showNoContentHelper() {\n        const { model } = this.props.list;\n        return this.props.noContentHelp && (model.useSampleModel || !model.hasData());\n    }\n\n    showGroupPager(group) {\n        return !group.isFolded && group.list.limit < group.list.count;\n    }\n\n    /**\n     * Returns true if the focus was toggled inside the same cell.\n     *\n     * @param {string} hotkey\n     * @param {HTMLTableCellElement} cell\n     */\n    toggleFocusInsideCell(hotkey, cell) {\n        if (![\"tab\", \"shift+tab\"].includes(hotkey) || !containsActiveElement(cell)) {\n            return false;\n        }\n        const focusableEls = getTabableElements(cell).filter(\n            (el) => el === document.activeElement || [\"INPUT\", \"TEXTAREA\"].includes(el.tagName)\n        );\n        const index = focusableEls.indexOf(document.activeElement);\n        return (\n            (hotkey === \"tab\" && index < focusableEls.length - 1) ||\n            (hotkey === \"shift+tab\" && index > 0)\n        );\n    }\n\n    async onGroupHeaderClicked(ev, group) {\n        const left = await this.props.list.leaveEditMode();\n        if (left) {\n            this.toggleGroup(group);\n        }\n    }\n\n    toggleGroup(group) {\n        group.toggle();\n    }\n\n    get canSelectRecord() {\n        return !this.editedRecord && !this.props.list.model.useSampleModel;\n    }\n\n    toggleSelection() {\n        const list = this.props.list;\n        if (!this.canSelectRecord) {\n            return;\n        }\n        return list.toggleSelection();\n    }\n\n    toggleRecordSelection(record, ev) {\n        if (!this.canSelectRecord) {\n            return;\n        }\n        const isRecordPresent = this.props.list.records.includes(this.lastCheckedRecord);\n        if (this.shiftKeyMode && isRecordPresent) {\n            this.toggleRecordShiftSelection(record);\n        } else {\n            record.toggleSelection();\n        }\n        this.lastCheckedRecord = record;\n    }\n\n    toggleRecordShiftSelection(record) {\n        const { records } = this.props.list;\n        const recordIndex = records.indexOf(record);\n        const lastCheckedRecordIndex = records.indexOf(this.lastCheckedRecord);\n        const start = Math.min(recordIndex, lastCheckedRecordIndex);\n        const end = Math.max(recordIndex, lastCheckedRecordIndex);\n        const { selected } = record;\n\n        for (let i = start; i <= end; i++) {\n            records[i].toggleSelection(!selected);\n        }\n    }\n\n    async toggleOptionalField(fieldName) {\n        this.optionalActiveFields[fieldName] = !this.optionalActiveFields[fieldName];\n        this.saveOptionalActiveFields(\n            this.allColumns.filter((col) => this.optionalActiveFields[col.name] && col.optional)\n        );\n        this.render();\n    }\n\n    toggleOptionalFieldGroup(groupId) {\n        const fieldNames = this.allColumns\n            .filter(\n                (col) =>\n                    col.type === \"field\" &&\n                    col.relatedPropertyField &&\n                    col.relatedPropertyField.id === groupId\n            )\n            .map((col) => col.name);\n        const active = !fieldNames.every((fieldName) => this.optionalActiveFields[fieldName]);\n        for (const fieldName of fieldNames) {\n            this.optionalActiveFields[fieldName] = active;\n        }\n        this.saveOptionalActiveFields(\n            this.allColumns.filter((col) => this.optionalActiveFields[col.name] && col.optional)\n        );\n        this.render();\n    }\n\n    toggleDebugOpenView() {\n        this.debugOpenView = !this.debugOpenView;\n        browser.localStorage.setItem(this.keyDebugOpenView, this.debugOpenView);\n        this.render();\n    }\n\n    onGlobalClick(ev) {\n        if (!this.editedRecord) {\n            return; // there's no row in edition\n        }\n\n        this.tableRef.el.querySelector(\"tbody\").classList.remove(\"o_keyboard_navigation\");\n\n        const target = ev.target;\n        if (this.tableRef.el.contains(target) && target.closest(\".o_data_row\")) {\n            // ignore clicks inside the table that are originating from a record row\n            // as they are handled directly by the renderer.\n            return;\n        }\n        if (this.activeElement !== this.uiService.activeElement) {\n            return;\n        }\n        // DateTime picker\n        if (target.closest(\".o_datetime_picker\")) {\n            return;\n        }\n        // Legacy autocomplete\n        if (ev.target.closest(\".ui-autocomplete\")) {\n            return;\n        }\n        this.props.list.leaveEditMode();\n    }\n\n    get isDebugMode() {\n        return Boolean(odoo.debug);\n    }\n\n    makeTooltip(column) {\n        return getTooltipInfo({\n            viewMode: \"list\",\n            resModel: this.props.list.resModel,\n            field: this.fields[column.name],\n            fieldInfo: column,\n        });\n    }\n\n    onColumnTitleMouseUp() {\n        if (this.columnWidths.resizing) {\n            this.preventReorder = true;\n        }\n    }\n\n    resetLongTouchTimer() {\n        if (this.longTouchTimer) {\n            browser.clearTimeout(this.longTouchTimer);\n            this.longTouchTimer = null;\n        }\n    }\n\n    onRowTouchStart(record, ev) {\n        if (!this.props.allowSelectors) {\n            return;\n        }\n        if (this.props.list.selection.length) {\n            ev.stopPropagation(); // This is done in order to prevent the tooltip from showing up\n        }\n        this.touchStartMs = Date.now();\n        if (this.longTouchTimer === null) {\n            this.longTouchTimer = browser.setTimeout(() => {\n                this.toggleRecordSelection(record);\n                this.resetLongTouchTimer();\n            }, this.constructor.LONG_TOUCH_THRESHOLD);\n        }\n    }\n    onRowTouchEnd(record) {\n        const elapsedTime = Date.now() - this.touchStartMs;\n        if (elapsedTime < this.constructor.LONG_TOUCH_THRESHOLD) {\n            this.resetLongTouchTimer();\n        }\n    }\n    onRowTouchMove(record) {\n        this.resetLongTouchTimer();\n    }\n\n    /**\n     * @param {string} dataRowId\n     * @param {Object} params\n     * @param {HTMLElement} params.element\n     * @param {HTMLElement} [params.group]\n     * @param {HTMLElement} [params.next]\n     * @param {HTMLElement} [params.parent]\n     * @param {HTMLElement} [params.previous]\n     */\n    async sortDrop(dataRowId, { element, previous }) {\n        await this.props.list.leaveEditMode();\n        element.classList.remove(\"o_row_draggable\");\n        const refId = previous ? previous.dataset.id : null;\n        try {\n            this.resequencePromise = this.props.list.resequence(dataRowId, refId, {\n                handleField: this.props.list.handleField,\n            });\n            await this.resequencePromise;\n        } finally {\n            element.classList.add(\"o_row_draggable\");\n        }\n    }\n\n    /**\n     * @param {Object} params\n     * @param {HTMLElement} params.element\n     * @param {HTMLElement} [params.group]\n     */\n    sortStart({ element }) {\n        const table = this.tableRef.el;\n        const headers = [...table.querySelectorAll(\"thead th\")];\n        const cells = [...element.querySelectorAll(\"td\")];\n        let headerIndex = 0;\n        for (const cell of cells) {\n            let width = 0;\n            for (let i = 0; i < cell.colSpan; i++) {\n                const header = headers[headerIndex + i];\n                const style = getComputedStyle(header);\n                width += parseFloat(style.width);\n            }\n            cell.style.width = `${width}px`;\n            headerIndex += cell.colSpan;\n        }\n    }\n\n    /**\n     * @param {Object} params\n     * @param {HTMLElement} params.element\n     * @param {HTMLElement} [params.group]\n     */\n    sortStop({ element }) {\n        for (const cell of element.querySelectorAll(\"td\")) {\n            cell.style.width = null;\n        }\n    }\n\n    ignoreEventInSelectionMode(ev) {\n        const { list } = this.props;\n        if (this.env.isSmall && list.selection && list.selection.length) {\n            // in selection mode, only selection is allowed.\n            ev.stopPropagation();\n            ev.preventDefault();\n        }\n    }\n\n    onClickCapture(record, ev) {\n        const { list } = this.props;\n        if (this.env.isSmall && list.selection && list.selection.length) {\n            ev.stopPropagation();\n            ev.preventDefault();\n            this.toggleRecordSelection(record);\n        }\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { RelationalModel } from \"@web/model/relational_model/relational_model\";\nimport { ListArchParser } from \"./list_arch_parser\";\nimport { ListController } from \"./list_controller\";\nimport { ListRenderer } from \"./list_renderer\";\n\nexport const listView = {\n    type: \"list\",\n    Controller: ListController,\n    Renderer: ListRenderer,\n    ArchParser: ListArchParser,\n    Model: RelationalModel,\n    buttonTemplate: \"web.ListView.Buttons\",\n    canOrderByCount: true,\n\n    limit: 80,\n\n    props: (genericProps, view) => {\n        const { ArchParser } = view;\n        const { arch, relatedModels, resModel } = genericProps;\n        const archInfo = new ArchParser().parse(arch, relatedModels, resModel);\n\n        return {\n            ...genericProps,\n            Model: view.Model,\n            Renderer: view.Renderer,\n            buttonTemplate: view.buttonTemplate,\n            archInfo,\n        };\n    },\n};\n\nregistry.category(\"views\").add(\"list\", listView);\n", "// TODO: add this in info props description\n\n// breadcrumbs: { type: Array, optional: true },\n// __getLocalState__: { type: CallbackRecorder, optional: true },\n// __getContext__: { type: CallbackRecorder, optional: true },\n// displayName: { type: String, optional: true },\n// noContentHelp: { type: String, optional: true },\n// searchViewId: { type: [Number, false], optional: true },\n// viewId: { type: [Number, false], optional: true },\n// views: { type: Array, element: Array, optional: true },\n// viewSwitcherEntries: { type: Array, optional: true },\n\nexport const standardViewProps = {\n    info: {\n        type: Object,\n    },\n    resModel: String,\n    arch: { type: Element },\n    bannerRoute: { type: String, optional: true },\n    className: { type: String, optional: true },\n    comparison: { type: [Object, { value: null }], optional: true },\n    context: { type: Object },\n    createRecord: { type: Function, optional: true },\n    display: { type: Object, optional: true },\n    domain: { type: Array },\n    fields: { type: Object },\n    globalState: { type: Object, optional: true },\n    groupBy: { type: Array, element: String },\n    limit: { type: Number, optional: true },\n    noBreadcrumbs: { type: Boolean, optional: true },\n    orderBy: { type: Array, element: Object },\n    relatedModels: { type: Object, optional: true },\n    resId: { type: [Number, Boolean], optional: true },\n    resIds: { type: Array, optional: true },\n    searchMenuTypes: { type: Array, element: String },\n    selectRecord: { type: Function, optional: true },\n    state: { type: Object, optional: true },\n    useSampleModel: { type: Boolean },\n    updateActionState: { type: Function, optional: true },\n};\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { combineModifiers } from \"@web/model/relational_model/utils\";\n\nexport const X2M_TYPES = [\"one2many\", \"many2many\"];\nconst NUMERIC_TYPES = [\"integer\", \"float\", \"monetary\"];\n\n/**\n * @typedef ViewActiveActions {\n * @property {\"view\"} type\n * @property {boolean} edit\n * @property {boolean} create\n * @property {boolean} delete\n * @property {boolean} duplicate\n */\n\nexport const BUTTON_CLICK_PARAMS = [\n    \"name\",\n    \"type\",\n    \"args\",\n    \"block-ui\", // Blocks UI with a spinner until the action is done\n    \"context\",\n    \"close\",\n    \"cancel-label\",\n    \"confirm\",\n    \"confirm-title\",\n    \"confirm-label\",\n    \"special\",\n    \"effect\",\n    \"help\",\n    // WOWL SAD: is adding the support for debounce attribute here justified or should we\n    // just override compileButton in kanban compiler to add the debounce?\n    \"debounce\",\n    // WOWL JPP: is adding the support for not oppening the dialog of confirmation in the settings view\n    // This should be refactor someday\n    \"noSaveDialog\",\n];\n\n/**\n * @param {string?} type\n * @returns {string | false}\n */\nfunction getViewClass(type) {\n    const isValidType = Boolean(type) && registry.category(\"views\").contains(type);\n    return isValidType && `o_${type}_view`;\n}\n\n/**\n * @param {string?} viewType\n * @param {Element?} rootNode\n * @param {string[]} additionalClassList\n * @returns {string}\n */\nexport function computeViewClassName(viewType, rootNode, additionalClassList = []) {\n    const subType = rootNode?.getAttribute(\"js_class\");\n    const classList = rootNode?.getAttribute(\"class\")?.split(\" \") || [];\n    const uniqueClasses = new Set([\n        getViewClass(viewType),\n        getViewClass(subType),\n        ...classList,\n        ...additionalClassList,\n    ]);\n    return Array.from(uniqueClasses)\n        .filter((c) => c) // remove falsy values\n        .join(\" \");\n}\n\n/**\n * TODO: doc\n *\n * @param {Object} fields\n * @param {Object} fieldAttrs\n * @param {string[]} activeMeasures\n * @returns {Object}\n */\nexport const computeReportMeasures = (\n    fields,\n    fieldAttrs,\n    activeMeasures,\n    { sumAggregatorOnly = false } = {}\n) => {\n    const measures = {\n        __count: { name: \"__count\", string: _t(\"Count\"), type: \"integer\" },\n    };\n    for (const [fieldName, field] of Object.entries(fields)) {\n        if (fieldName === \"id\") {\n            continue;\n        }\n        const { isInvisible } = fieldAttrs[fieldName] || {};\n        if (isInvisible) {\n            continue;\n        }\n        if (\n            [\"integer\", \"float\", \"monetary\"].includes(field.type) &&\n            ((sumAggregatorOnly && field.aggregator === \"sum\") ||\n                (!sumAggregatorOnly && field.aggregator))\n        ) {\n            measures[fieldName] = field;\n        }\n    }\n\n    // add active measures to the measure list.  This is very rarely\n    // necessary, but it can be useful if one is working with a\n    // functional field non stored, but in a model with an overridden\n    // read_group method.  In this case, the pivot view could work, and\n    // the measure should be allowed.  However, be careful if you define\n    // a measure in your pivot view: non stored functional fields will\n    // probably not work (their aggregate will always be 0).\n    for (const measure of activeMeasures) {\n        if (!measures[measure]) {\n            measures[measure] = fields[measure];\n        }\n    }\n\n    for (const fieldName in fieldAttrs) {\n        if (fieldAttrs[fieldName].string && fieldName in measures) {\n            measures[fieldName].string = fieldAttrs[fieldName].string;\n        }\n    }\n\n    const sortedMeasures = Object.entries(measures).sort(([m1, f1], [m2, f2]) => {\n        if (m1 === \"__count\" || m2 === \"__count\") {\n            return m1 === \"__count\" ? 1 : -1; // Count is always last\n        }\n        return f1.string.toLowerCase().localeCompare(f2.string.toLowerCase());\n    });\n\n    return Object.fromEntries(sortedMeasures);\n};\n\n/**\n * @param {Record} record\n * @param {String} fieldName\n * @param {Object} [fieldInfo]\n * @returns {String}\n */\nexport function getFormattedValue(record, fieldName, fieldInfo = null) {\n    const field = record.fields[fieldName];\n    const formatter = registry.category(\"formatters\").get(field.type, (val) => val);\n    const formatOptions = {};\n    if (fieldInfo && formatter.extractOptions) {\n        Object.assign(formatOptions, formatter.extractOptions(fieldInfo));\n    }\n    formatOptions.data = record.data;\n    formatOptions.field = field;\n    return record.data[fieldName] !== undefined\n        ? formatter(record.data[fieldName], formatOptions)\n        : \"\";\n}\n\n/**\n * @param {Element} rootNode\n * @returns {ViewActiveActions}\n */\nexport function getActiveActions(rootNode) {\n    const activeActions = {\n        type: \"view\",\n        edit: exprToBoolean(rootNode.getAttribute(\"edit\"), true),\n        create: exprToBoolean(rootNode.getAttribute(\"create\"), true),\n        delete: exprToBoolean(rootNode.getAttribute(\"delete\"), true),\n    };\n    activeActions.duplicate =\n        activeActions.create && exprToBoolean(rootNode.getAttribute(\"duplicate\"), true);\n    return activeActions;\n}\n\nexport function getClassNameFromDecoration(decoration) {\n    if (decoration === \"bf\") {\n        return \"fw-bold\";\n    } else if (decoration === \"it\") {\n        return \"fst-italic\";\n    }\n    return `text-${decoration}`;\n}\n\nexport function getDecoration(rootNode) {\n    const decorations = [];\n    for (const name of rootNode.getAttributeNames()) {\n        if (name.startsWith(\"decoration-\")) {\n            decorations.push({\n                class: getClassNameFromDecoration(name.replace(\"decoration-\", \"\")),\n                condition: rootNode.getAttribute(name),\n            });\n        }\n    }\n    return decorations;\n}\n\n/**\n * @param {any} field\n * @returns {boolean}\n */\nexport function isX2Many(field) {\n    return field && X2M_TYPES.includes(field.type);\n}\n\n/**\n * @param {Object} field\n * @returns {boolean} true iff the given field is a numeric field\n */\nexport function isNumeric(field) {\n    return NUMERIC_TYPES.includes(field.type);\n}\n\n/**\n * @param {any} value\n * @returns {boolean}\n */\nexport function isNull(value) {\n    return [null, undefined].includes(value);\n}\n\nexport function processButton(node) {\n    const withDefault = {\n        close: (val) => exprToBoolean(val, false),\n        context: (val) => val || \"{}\",\n    };\n    const clickParams = {};\n    const attrs = {};\n    for (const { name, value } of node.attributes) {\n        if (BUTTON_CLICK_PARAMS.includes(name)) {\n            clickParams[name] = withDefault[name] ? withDefault[name](value) : value;\n        } else {\n            attrs[name] = value;\n        }\n    }\n    return {\n        className: node.getAttribute(\"class\") || \"\",\n        disabled: !!node.getAttribute(\"disabled\") || false,\n        icon: node.getAttribute(\"icon\") || false,\n        title: node.getAttribute(\"title\") || undefined,\n        string: node.getAttribute(\"string\") || undefined,\n        options: JSON.parse(node.getAttribute(\"options\") || \"{}\"),\n        display: node.getAttribute(\"display\") || \"selection\",\n        clickParams,\n        column_invisible: node.getAttribute(\"column_invisible\"),\n        invisible: combineModifiers(\n            node.getAttribute(\"column_invisible\"),\n            node.getAttribute(\"invisible\"),\n            \"OR\"\n        ),\n        readonly: node.getAttribute(\"readonly\"),\n        required: node.getAttribute(\"required\"),\n        attrs,\n    };\n}\n\n/**\n * In the preview implementation of reporting views, the virtual field used to\n * display the number of records was named __count__, whereas __count is\n * actually the one used in xml. So basically, activating a filter specifying\n * __count as measures crashed. Unfortunately, as __count__ was used in the JS,\n * all filters saved as favorite at that time were saved with __count__, and\n * not __count. So in order the make them still work with the new\n * implementation, we handle both __count__ and __count.\n *\n * This function replaces occurences of '__count__' by '__count' in the given\n * element(s).\n *\n * @param {any | any[]} [measures]\n * @returns {any}\n */\nexport function processMeasure(measure) {\n    if (Array.isArray(measure)) {\n        return measure.map(processMeasure);\n    }\n    return measure === \"__count__\" ? \"__count\" : measure;\n}\n\n/**\n * Transforms a string into a valid expression to be injected\n * in a template as a props via setAttribute.\n * Example: myString = `Some weird language quote (\") `;\n *     should become in the template:\n *      <Component label=\"&quot;Some weird language quote (\\\\&quot;)&quot; \" />\n *     which should be interpreted by owl as a JS expression being a string:\n *      `Some weird language quote (\") `\n *\n * @param  {string} str The initial value: a pure string to be interpreted as such\n * @return {string}     the valid string to be injected into a component's node props.\n */\nexport function toStringExpression(str) {\n    return `\\`${str.replaceAll(\"`\", \"\\\\`\")}\\``;\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 { useDebugCategory } from \"@web/core/debug/debug_context\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { registry } from \"@web/core/registry\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { deepCopy, pick } from \"@web/core/utils/objects\";\nimport { nbsp } from \"@web/core/utils/strings\";\nimport { parseXML } from \"@web/core/utils/xml\";\nimport { extractLayoutComponents } from \"@web/search/layout\";\nimport { WithSearch } from \"@web/search/with_search/with_search\";\nimport { useActionLinks } from \"@web/views/view_hook\";\nimport { computeViewClassName } from \"./utils\";\nimport { loadBundle } from \"@web/core/assets\";\nimport { cookie } from \"@web/core/browser/cookie\";\nimport {\n    Component,\n    markRaw,\n    onWillUpdateProps,\n    onWillStart,\n    toRaw,\n    useSubEnv,\n    reactive,\n} from \"@odoo/owl\";\nimport { session } from \"@web/session\";\n\n/**\n * @typedef Config\n * @property {number | false} actionId\n * @property {string | false} actionType\n * @property {Record<string, any>} actionFlags\n * @property {() => []} breadcrumbs\n * @property {() => string} getDisplayName\n * @property {(string) => any} setDisplayName\n * @property {() => Record<string, any>} getPagerProps\n * @property {Record<string, any>[]} viewSwitcherEntry\n * @property {typeof Component} Banner\n *\n * @typedef {import(\"@web/env\").OdooEnv} OdooEnv\n * @typedef {import(\"./utils\").OrderTerm} OrderTerm\n *\n * @typedef ViewProps\n * @property {string} resModel\n * @property {ViewType} type\n *\n * @property {string} [arch] if given, fields must be given too /\\ no post processing is done (evaluation of \"groups\" attribute,...)\n * @property {Record<string, any>} [fields] if given, arch must be given too\n * @property {number|false} [viewId]\n * @property {Record<string, any>} [actionMenus]\n * @property {boolean} [loadActionMenus=false]\n *\n * @property {string} [searchViewArch] if given, searchViewFields must be given too\n * @property {Record<string, any>} [searchViewFields] if given, searchViewArch must be given too\n * @property {number|false} [searchViewId]\n * @property {Record<string, any>[]} [irFilters]\n * @property {boolean} [loadIrFilters=false]\n *\n * @property {Record<string, any>} [comparison]\n * @property {Record<any, any>} [context={}]\n * @property {DomainRepr} [domain]\n * @property {string[]} [groupBy]\n * @property {OrderTerm[]} [orderBy]\n *\n * @property {boolean} [useSampleModel]\n * @property {string} [noContentHelp]\n *\n * @property {Record<string, any>} [display={}] to rework\n *\n * --- Manipulated by withSearch ---\n * @property {boolean} [activateFavorite]\n * @property {Record<string, any>[]} [dynamicFilters]\n * @property {boolean} [hideCustomGroupBy]\n * @property {string[]} [searchMenuTypes]\n * @property {Record<string, any>} [globalState]\n *\n * @typedef {\"activity\"\n *  | \"calendar\"\n *  | \"cohort\"\n *  | \"form\"\n *  | \"gantt\"\n *  | \"graph\"\n *  | \"grid\"\n *  | \"hierarchy\"\n *  | \"kanban\"\n *  | \"list\"\n *  | \"map\"\n *  | \"pivot\"\n *  | \"search\"\n * } ViewType\n */\n\nconst viewRegistry = registry.category(\"views\");\n\nviewRegistry.addValidation({\n    type: { validate: (t) => t in session.view_info },\n    Controller: { validate: (c) => c.prototype instanceof Component },\n    \"*\": true,\n});\n\n/**\n * Returns the default config to use if no config, or an incomplete config has\n * been provided in the env, which can happen with standalone views.\n * @returns {Config}\n */\nexport function getDefaultConfig() {\n    let displayName;\n    const config = {\n        actionId: false,\n        actionType: false,\n        embeddedActions: [],\n        currentEmbeddedActionId: false,\n        parentActionId: false,\n        actionFlags: {},\n        breadcrumbs: reactive([\n            {\n                get name() {\n                    return displayName;\n                },\n            },\n        ]),\n        disableSearchBarAutofocus: false,\n        getDisplayName: () => displayName,\n        historyBack: () => {},\n        pagerProps: {},\n        setDisplayName: (newDisplayName) => {\n            displayName = newDisplayName;\n            // This is a hack to force the reactivity when a new displayName is set\n            config.breadcrumbs.push(undefined);\n            config.breadcrumbs.pop();\n        },\n        viewSwitcherEntries: [],\n        views: [],\n    };\n    return config;\n}\n\nexport class ViewNotFoundError extends Error {}\n\nconst CALLBACK_RECORDER_NAMES = [\n    \"__beforeLeave__\",\n    \"__getGlobalState__\",\n    \"__getLocalState__\",\n    \"__getContext__\",\n    \"__getOrderBy__\",\n];\n\nconst STANDARD_PROPS = [\n    \"resModel\",\n    \"type\",\n    \"jsClass\",\n\n    \"arch\",\n    \"fields\",\n    \"relatedModels\",\n    \"viewId\",\n    \"views\",\n    \"actionMenus\",\n    \"loadActionMenus\",\n\n    \"searchViewArch\",\n    \"searchViewFields\",\n    \"searchViewId\",\n    \"irFilters\",\n    \"loadIrFilters\",\n\n    \"comparison\",\n    \"context\",\n    \"domain\",\n    \"groupBy\",\n    \"orderBy\",\n\n    \"useSampleModel\",\n    \"noContentHelp\",\n    \"className\",\n\n    \"display\",\n    \"globalState\",\n\n    \"activateFavorite\",\n    \"dynamicFilters\",\n    \"hideCustomGroupBy\",\n    \"searchMenuTypes\",\n\n    ...CALLBACK_RECORDER_NAMES,\n\n    // LEGACY: remove this later (clean when mappings old state <-> new state are established)\n    \"searchPanel\",\n    \"searchModel\",\n];\n\nconst ACTIONS = [\"create\", \"delete\", \"edit\", \"group_create\", \"group_delete\", \"group_edit\"];\n\n/** @extends {Component<ViewProps, import(\"@web/env\").OdooEnv>} */\nexport class View extends Component {\n    static _download = async function () {};\n    static template = \"web.View\";\n    static components = { WithSearch };\n    static searchMenuTypes = [\"filter\", \"groupBy\", \"favorite\"];\n    static canOrderByCount = false;\n    static defaultProps = {\n        display: {},\n        context: {},\n        loadActionMenus: false,\n        loadIrFilters: false,\n        className: \"\",\n    };\n    static props = {\n        \"*\": true,\n    };\n\n    setup() {\n        const { arch, fields, resModel, searchViewArch, searchViewFields, type } = this.props;\n        if (!resModel) {\n            throw Error(`View props should have a \"resModel\" key`);\n        }\n        if (!type) {\n            throw Error(`View props should have a \"type\" key`);\n        }\n        if ((arch && !fields) || (!arch && fields)) {\n            throw new Error(`\"arch\" and \"fields\" props must be given together`);\n        }\n        if ((searchViewArch && !searchViewFields) || (!searchViewArch && searchViewFields)) {\n            throw new Error(`\"searchViewArch\" and \"searchViewFields\" props must be given together`);\n        }\n\n        this.viewService = useService(\"view\");\n        this.withSearchProps = null;\n\n        useSubEnv({\n            keepLast: new KeepLast(),\n            config: {\n                ...getDefaultConfig(),\n                ...this.env.config,\n            },\n            ...Object.fromEntries(\n                CALLBACK_RECORDER_NAMES.map((name) => [name, this.props[name] || null])\n            ),\n        });\n\n        this.handleActionLinks = useActionLinks({ resModel });\n\n        onWillStart(() => this.loadView(this.props));\n        onWillUpdateProps((nextProps) => this.onWillUpdateProps(nextProps));\n\n        useDebugCategory(\"view\", { component: this });\n    }\n\n    /**\n     * @param {ViewProps} props\n     */\n    async loadView(props) {\n        const type = props.type;\n\n        if (!session.view_info[type]) {\n            throw new Error(`Invalid view type: ${type}`);\n        }\n\n        // determine views for which descriptions should be obtained\n        let { viewId, searchViewId } = props;\n\n        const views = deepCopy(props.views || this.env.config.views);\n        const view = views.find((v) => v[1] === type) || [];\n        if (view.length) {\n            view[0] = viewId !== undefined ? viewId : view[0];\n            viewId = view[0];\n        } else {\n            view.push(viewId || false, type);\n            views.push(view); // viewId will remain undefined if not specified and loadView=false\n        }\n\n        const searchView = views.find((v) => v[1] === \"search\");\n        if (searchView) {\n            searchView[0] = searchViewId !== undefined ? searchViewId : searchView[0];\n            searchViewId = searchView[0];\n        } else if (searchViewId !== undefined) {\n            views.push([searchViewId, \"search\"]);\n        }\n        // searchViewId will remains undefined if loadSearchView=false\n\n        // prepare view description\n        const { context, resModel, loadActionMenus, loadIrFilters } = props;\n        let {\n            arch,\n            fields,\n            relatedModels,\n            searchViewArch,\n            searchViewFields,\n            irFilters,\n            actionMenus,\n        } = props;\n\n        const loadView = !arch || (!actionMenus && loadActionMenus);\n        const loadSearchView =\n            (searchViewId !== undefined && !searchViewArch) || (!irFilters && loadIrFilters);\n\n        let viewDescription = { viewId, resModel, type };\n        let searchViewDescription;\n        if (loadView || loadSearchView) {\n            // view description (or search view description if required) is incomplete\n            // a loadViews is done to complete the missing information\n            const options = {\n                actionId: this.env.config.actionId,\n                loadActionMenus,\n                loadIrFilters,\n            };\n            if (this.env.config.currentEmbeddedActionId) {\n                options.embeddedActionId = this.env.config.currentEmbeddedActionId;\n                options.embeddedParentResId = context.active_id;\n            }\n            const result = await this.viewService.loadViews({ context, resModel, views }, options);\n            // Note: if props.views is different from views, the cached descriptions\n            // will certainly not be reused! (but for the standard flow this will work as\n            // before)\n            viewDescription = result.views[type];\n            searchViewDescription = result.views.search;\n            if (loadSearchView) {\n                searchViewId = searchViewId || searchViewDescription.id;\n                if (!searchViewArch) {\n                    searchViewArch = searchViewDescription.arch;\n                    searchViewFields = result.fields;\n                }\n                if (!irFilters) {\n                    irFilters = searchViewDescription.irFilters;\n                }\n            }\n            this.env.config.views = views;\n            fields = fields || markRaw(result.fields);\n            relatedModels = relatedModels || markRaw(result.relatedModels);\n        }\n\n        if (!arch) {\n            arch = viewDescription.arch;\n        }\n        if (!actionMenus) {\n            actionMenus = viewDescription.actionMenus;\n        }\n\n        const archXmlDoc = parseXML(arch.replace(/&amp;nbsp;/g, nbsp));\n        for (const action of ACTIONS) {\n            if (action in this.props.context && !this.props.context[action]) {\n                archXmlDoc.setAttribute(action, \"0\");\n            }\n        }\n\n        const jsClass = archXmlDoc.hasAttribute(\"js_class\")\n            ? archXmlDoc.getAttribute(\"js_class\")\n            : props.jsClass || type;\n        if (!viewRegistry.contains(jsClass)) {\n            await loadBundle(\n                cookie.get(\"color_scheme\") === \"dark\"\n                    ? \"web.assets_backend_lazy_dark\"\n                    : \"web.assets_backend_lazy\"\n            );\n        }\n        const descr = viewRegistry.get(jsClass);\n\n        const sample = archXmlDoc.getAttribute(\"sample\");\n        const className = computeViewClassName(type, archXmlDoc, [\n            \"o_view_controller\",\n            ...(props.className || \"\").split(\" \"),\n        ]);\n\n        Object.assign(this.env.config, {\n            rawArch: arch,\n            viewArch: archXmlDoc,\n            viewId: viewDescription.id,\n            viewType: type,\n            viewSubType: jsClass,\n            noBreadcrumbs: props.noBreadcrumbs,\n            ...extractLayoutComponents(descr),\n        });\n        const info = {\n            actionMenus,\n            mode: props.display.mode,\n            irFilters,\n            searchViewArch,\n            searchViewFields,\n            searchViewId,\n        };\n\n        // prepare the view props\n        const viewProps = {\n            info,\n            arch: archXmlDoc,\n            fields,\n            relatedModels,\n            resModel,\n            useSampleModel: false,\n            className,\n        };\n        if (viewDescription.custom_view_id) {\n            // for dashboard\n            viewProps.info.customViewId = viewDescription.custom_view_id;\n        }\n        if (props.globalState) {\n            viewProps.globalState = props.globalState;\n        }\n\n        if (\"useSampleModel\" in props) {\n            viewProps.useSampleModel = props.useSampleModel;\n        } else if (sample) {\n            viewProps.useSampleModel = evaluateBooleanExpr(sample);\n        }\n\n        for (const key in props) {\n            if (!STANDARD_PROPS.includes(key)) {\n                viewProps[key] = props[key];\n            }\n        }\n\n        const { noContentHelp } = props;\n        if (noContentHelp) {\n            viewProps.info.noContentHelp = noContentHelp;\n        }\n\n        const searchMenuTypes =\n            props.searchMenuTypes || descr.searchMenuTypes || this.constructor.searchMenuTypes;\n        viewProps.searchMenuTypes = searchMenuTypes;\n        const canOrderByCount = descr.canOrderByCount || this.constructor.canOrderByCount;\n\n        const finalProps = descr.props ? descr.props(viewProps, descr, this.env.config) : viewProps;\n        // prepare the WithSearch component props\n        this.Controller = descr.Controller;\n        this.componentProps = finalProps;\n        this.withSearchProps = {\n            ...toRaw(props),\n            hideCustomGroupBy: props.hideCustomGroupBy || descr.hideCustomGroupBy,\n            searchMenuTypes,\n            canOrderByCount,\n            SearchModel: descr.SearchModel,\n        };\n\n        if (searchViewId !== undefined) {\n            this.withSearchProps.searchViewId = searchViewId;\n        }\n        if (searchViewArch) {\n            this.withSearchProps.searchViewArch = searchViewArch;\n            this.withSearchProps.searchViewFields = searchViewFields;\n        }\n        if (irFilters) {\n            this.withSearchProps.irFilters = irFilters;\n        }\n\n        if (descr.display) {\n            // FIXME: there's something inelegant here: display might come from\n            // the View's defaultProps, in which case, modifying it in place\n            // would have unwanted effects.\n            const viewDisplay = deepCopy(descr.display);\n            const display = { ...this.withSearchProps.display };\n            for (const key in viewDisplay) {\n                if (typeof display[key] === \"object\") {\n                    Object.assign(display[key], viewDisplay[key]);\n                } else if (!(key in display) || display[key]) {\n                    display[key] = viewDisplay[key];\n                }\n            }\n            this.withSearchProps.display = display;\n        }\n\n        for (const key in this.withSearchProps) {\n            if (!(key in WithSearch.props)) {\n                delete this.withSearchProps[key];\n            }\n        }\n    }\n\n    /**\n     * @param {ViewProps} nextProps\n     */\n    onWillUpdateProps(nextProps) {\n        const oldProps = pick(this.props, \"arch\", \"type\", \"resModel\");\n        const newProps = pick(nextProps, \"arch\", \"type\", \"resModel\");\n        if (JSON.stringify(oldProps) !== JSON.stringify(newProps)) {\n            return this.loadView(nextProps);\n        }\n        // we assume that nextProps can only vary in the search keys:\n        // comparison, context, domain, groupBy, orderBy\n        const { comparison, context, domain, groupBy, orderBy } = nextProps;\n        Object.assign(this.withSearchProps, { comparison, context, domain, groupBy, orderBy });\n    }\n}\n", "import { ViewButton } from \"./view_button\";\n\nexport class MultiRecordViewButton extends ViewButton {\n    static props = [...ViewButton.props, \"list\", \"domain\"];\n\n    async onClick() {\n        const { clickParams, list } = this.props;\n        const resIds = await list.getResIds(true);\n        clickParams.buttonContext = {\n            active_domain: this.props.domain,\n            active_ids: resIds,\n            active_model: list.resModel,\n        };\n\n        this.env.onClickViewButton({\n            clickParams,\n            getResParams: () => ({\n                context: list.context,\n                evalContext: list.evalContext,\n                resModel: list.resModel,\n                resIds,\n            }),\n        });\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { useDropdownCloser } from \"@web/core/dropdown/dropdown_hooks\";\nimport { pick } from \"@web/core/utils/objects\";\nimport { debounce as debounceFn } from \"@web/core/utils/timing\";\n\nconst explicitRankClasses = [\n    \"btn-primary\",\n    \"btn-secondary\",\n    \"btn-link\",\n    \"btn-success\",\n    \"btn-info\",\n    \"btn-warning\",\n    \"btn-danger\",\n];\n\nconst odooToBootstrapClasses = {\n    oe_highlight: \"btn-primary\",\n    oe_link: \"btn-link\",\n};\n\nfunction iconFromString(iconString) {\n    const icon = {};\n    if (iconString.startsWith(\"fa-\")) {\n        icon.tag = \"i\";\n        icon.class = `o_button_icon fa fa-fw ${iconString}`;\n    } else if (iconString.startsWith(\"oi-\")) {\n        icon.tag = \"i\";\n        icon.class = `o_button_icon oi oi-fw ${iconString}`;\n    } else {\n        icon.tag = \"img\";\n        icon.src = iconString;\n    }\n    return icon;\n}\n\nexport class ViewButton extends Component {\n    static template = \"web.views.ViewButton\";\n    static props = [\n        \"id?\",\n        \"tag?\",\n        \"record?\",\n        \"attrs?\",\n        \"className?\",\n        \"context?\",\n        \"clickParams?\",\n        \"icon?\",\n        \"defaultRank?\",\n        \"disabled?\",\n        \"size?\",\n        \"tabindex?\",\n        \"title?\",\n        \"style?\",\n        \"string?\",\n        \"slots?\",\n        \"onClick?\",\n    ];\n    static defaultProps = {\n        tag: \"button\",\n        className: \"\",\n        clickParams: {},\n        attrs: {},\n    };\n\n    setup() {\n        if (this.props.icon) {\n            this.icon = iconFromString(this.props.icon);\n        }\n        const { debounce } = this.clickParams;\n        if (debounce) {\n            this.onClick = debounceFn(this.onClick.bind(this), debounce, true);\n        }\n        this.tooltip = JSON.stringify({\n            debug: Boolean(odoo.debug),\n            button: {\n                string: this.props.string,\n                help: this.clickParams.help,\n                context: this.clickParams.context,\n                invisible: this.props.attrs.invisible,\n                column_invisible: this.props.attrs.column_invisible,\n                readonly: this.props.attrs.readonly,\n                required: this.props.attrs.required,\n                special: this.clickParams.special,\n                type: this.clickParams.type,\n                name: this.clickParams.name,\n                title: this.props.title,\n            },\n            context: this.props.record && this.props.record.context,\n            model: this.props.record && this.props.record.resModel,\n        });\n        this.dropdownControl = useDropdownCloser();\n    }\n\n    get clickParams() {\n        return { context: this.props.context, ...this.props.clickParams };\n    }\n\n    get hasBigTooltip() {\n        return Boolean(odoo.debug) || this.clickParams.help;\n    }\n\n    get hasSmallToolTip() {\n        return !this.hasBigTooltip && this.props.title;\n    }\n\n    get disabled() {\n        const { name, type, special } = this.clickParams;\n        return (!name && !type && !special) || this.props.disabled;\n    }\n\n    /**\n     * @param {MouseEvent} ev\n     */\n    onClick(ev) {\n        if (this.props.tag === \"a\") {\n            ev.preventDefault();\n        }\n\n        if (this.props.onClick) {\n            return this.props.onClick();\n        }\n\n        this.env.onClickViewButton({\n            clickParams: this.clickParams,\n            getResParams: () =>\n                pick(\n                    this.props.record || {},\n                    \"context\",\n                    \"evalContext\",\n                    \"resModel\",\n                    \"resId\",\n                    \"resIds\"\n                ),\n            beforeExecute: () => this.dropdownControl.close(),\n        });\n    }\n\n    getClassName() {\n        const classNames = [];\n        let hasExplicitRank = false;\n        if (this.props.className) {\n            for (let cls of this.props.className.split(\" \")) {\n                if (cls in odooToBootstrapClasses) {\n                    cls = odooToBootstrapClasses[cls];\n                }\n                classNames.push(cls);\n                if (!hasExplicitRank && explicitRankClasses.includes(cls)) {\n                    hasExplicitRank = true;\n                }\n            }\n        }\n        if (this.props.tag === \"button\") {\n            const hasOtherClasses = classNames.length;\n            classNames.unshift(\"btn\");\n            if ((!hasExplicitRank && this.props.defaultRank) || !hasOtherClasses) {\n                classNames.push(this.props.defaultRank || \"btn-secondary\");\n            }\n            if (this.props.size) {\n                classNames.push(`btn-${this.props.size}`);\n            }\n        }\n        return classNames.join(\" \");\n    }\n}\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { evaluateExpr } from \"@web/core/py_js/py\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\n\nimport { status, useComponent, useEnv, useSubEnv } from \"@odoo/owl\";\n\nexport async function executeButtonCallback(el, fct) {\n    let btns = [];\n    function disableButtons() {\n        btns = [\n            ...btns,\n            ...el.querySelectorAll(\"button:not([disabled])\"),\n            ...document.querySelectorAll(\".o-overlay-container button:not([disabled])\"),\n        ];\n        for (const btn of btns) {\n            btn.setAttribute(\"disabled\", \"1\");\n        }\n    }\n\n    function enableButtons() {\n        for (const btn of btns) {\n            btn.removeAttribute(\"disabled\");\n        }\n    }\n\n    disableButtons();\n    let res;\n    try {\n        res = await fct();\n    } finally {\n        enableButtons();\n    }\n    return res;\n}\n\nfunction undefinedAsTrue(val) {\n    return typeof val === \"undefined\" || val;\n}\n\n/**\n * @typedef {Object} Options\n * @property {Function} [afterExecuteAction]\n * @property {Function} [beforeExecuteAction]\n * @property {Function} [reload]\n */\n\n/**\n * @param {{ readonly el: HTMLElement | null; }} ref\n * @param {Options} [options={}]\n */\nexport function useViewButtons(ref, options = {}) {\n    const action = useService(\"action\");\n    const dialog = useService(\"dialog\");\n    const comp = useComponent();\n    const env = useEnv();\n    useSubEnv({\n        async onClickViewButton({ clickParams, getResParams, beforeExecute }) {\n            async function execute() {\n                let _continue = true;\n                if (beforeExecute) {\n                    _continue = undefinedAsTrue(await beforeExecute());\n                }\n\n                _continue =\n                    _continue && undefinedAsTrue(await options.beforeExecuteAction?.(clickParams));\n                if (!_continue) {\n                    return;\n                }\n                const closeDialog =\n                    (clickParams.close || clickParams.special) && env.dialogData?.close;\n                const params = getResParams();\n                let buttonContext = {};\n                if (clickParams.context) {\n                    if (typeof clickParams.context === \"string\") {\n                        buttonContext = evaluateExpr(clickParams.context, params.evalContext);\n                    } else {\n                        buttonContext = clickParams.context;\n                    }\n                }\n                if (clickParams.buttonContext) {\n                    Object.assign(buttonContext, clickParams.buttonContext);\n                }\n                const doActionParams = Object.assign({}, clickParams, {\n                    resModel: params.resModel,\n                    resId: params.resId,\n                    resIds: params.resIds,\n                    context: params.context || {},\n                    buttonContext,\n                    onClose: async (onCloseInfo) => {\n                        if (\n                            !closeDialog &&\n                            status(comp) !== \"destroyed\" &&\n                            !onCloseInfo?.noReload\n                        ) {\n                            await options.reload?.();\n                        }\n                    },\n                });\n                let error;\n                try {\n                    await action.doActionButton(doActionParams);\n                } catch (_e) {\n                    error = _e;\n                }\n                await options.afterExecuteAction?.(clickParams);\n                if (closeDialog) {\n                    closeDialog();\n                }\n                if (error) {\n                    return Promise.reject(error);\n                }\n            }\n\n            if (clickParams.confirm) {\n                executeButtonCallback(getEl(), async () => {\n                    await new Promise((resolve) => {\n                        const dialogProps = {\n                            ...(clickParams[\"confirm-title\"] && {\n                                title: clickParams[\"confirm-title\"],\n                            }),\n                            ...(clickParams[\"confirm-label\"] && {\n                                confirmLabel: clickParams[\"confirm-label\"],\n                            }),\n                            ...(clickParams[\"cancel-label\"] && {\n                                cancelLabel: clickParams[\"cancel-label\"],\n                            }),\n                            body: clickParams.confirm,\n                            confirm: () => execute(),\n                            cancel: () => {},\n                        };\n                        dialog.add(ConfirmationDialog, dialogProps, { onClose: resolve });\n                    });\n                });\n            } else {\n                return executeButtonCallback(getEl(), execute);\n            }\n        },\n    });\n\n    function getEl() {\n        if (env.inDialog) {\n            const el = ref.el;\n            return el ? el.closest(\".modal\") : null;\n        } else {\n            return ref.el;\n        }\n    }\n}\n", "import {\n    append,\n    combineAttributes,\n    createElement,\n    createTextNode,\n    getTag,\n} from \"@web/core/utils/xml\";\nimport { toStringExpression, BUTTON_CLICK_PARAMS } from \"./utils\";\n\n/**\n * @typedef Compiler\n * @property {string} selector\n * @property {(el: Element, params: Record<string, any>) => Element} fn\n * @property {string} [class]\n * @property {boolean} [doNotCopyAttributes]\n */\n\nimport { xml } from \"@odoo/owl\";\n\nconst BUTTON_STRING_PROPS = [\"string\", \"size\", \"title\", \"icon\", \"id\", \"disabled\"];\nconst INTERP_REGEXP = /(\\{\\{|#\\{)(.*?)(\\}{1,2})/g;\n\n/**\n * @param {string} str\n * @returns {string} the interpolated string to be injected into a component's node props.\n */\nexport function toInterpolatedStringExpression(str) {\n    const matches = str.matchAll(INTERP_REGEXP);\n    const parts = [];\n    let searchString = str;\n    for (const [match, head, expr] of matches) {\n        const index = searchString.indexOf(head);\n        const left = searchString.slice(0, index);\n        if (left) {\n            parts.push(toStringExpression(left));\n        }\n        parts.push(`(${expr})`);\n        searchString = searchString.slice(index + match.length);\n    }\n    parts.push(toStringExpression(searchString));\n    return parts.join(\"+\");\n}\n\n/**\n * @param {Element} el\n * @param {string} attr\n * @param {string} string\n */\nexport function appendAttr(el, attr, string) {\n    const attrKey = `t-att-${attr}`;\n    const attrVal = el.getAttribute(attrKey);\n    el.setAttribute(attrKey, appendToStringifiedObject(attrVal, string));\n}\n\n/**\n * @param {string} originalTattr\n * @param {string} string\n * @returns {string}\n */\nfunction appendToStringifiedObject(originalTattr, string) {\n    const re = /{(.*)}/;\n    const oldString = re.exec(originalTattr);\n\n    if (oldString) {\n        string = `${oldString[1]},${string}`;\n    }\n    return `{${string}}`;\n}\n\n/**\n * @param {Element} target\n * @param  {...Element} sources\n * @returns {Element}\n */\nexport function assignOwlDirectives(target, ...sources) {\n    for (const source of sources) {\n        for (const { name, value } of source.attributes) {\n            if (name.startsWith(\"t-attf-\")) {\n                const propName = name.slice(7);\n                const interpolatedExpression = toInterpolatedStringExpression(value);\n                target.setAttribute(propName, interpolatedExpression);\n            } else if (name.startsWith(\"t-att-\")) {\n                const propName = name.slice(6);\n                target.setAttribute(propName, value);\n            } else if (name.startsWith(\"t-\")) {\n                target.setAttribute(name, value);\n            }\n        }\n    }\n    return target;\n}\n\n/**\n * @param {Element} el\n * @param {Element} compiled\n */\nexport function copyAttributes(el, compiled) {\n    const isComponent = isComponentNode(compiled);\n    const classes = el.className;\n    if (classes) {\n        if (isComponent) {\n            const cls = compiled.className;\n            compiled.setAttribute(\"class\", cls ? `'${classes} ' + ${cls}` : `'${classes}'`);\n        } else {\n            compiled.classList.add(...classes.split(/\\s+/).filter(Boolean));\n        }\n    }\n\n    let att = el.getAttribute(\"style\");\n    if (att) {\n        if (isComponent) {\n            att = toStringExpression(att);\n        }\n        compiled.setAttribute(\"style\", att);\n    }\n}\n\n/**\n * Decodes a string within an attribute into an Object\n * @param  {string} str\n * @return {Object}\n */\nexport function decodeObjectForTemplate(str) {\n    return JSON.parse(decodeURI(str));\n}\n\n/**\n * Encodes an object into a string usable inside a pre-compiled template\n * @param  {Object}\n * @return {string}\n */\nexport function encodeObjectForTemplate(obj) {\n    return `\"${encodeURI(JSON.stringify(obj))}\"`;\n}\n\n/**\n * @param {Element} el\n * @param {string} modifierName\n * @returns {boolean | boolean[]}\n */\nexport function getModifier(el, modifierName) {\n    return el.getAttribute(modifierName);\n}\n\n/**\n * @param {any} node\n * @returns {string}\n */\nfunction getTitleTag(node) {\n    return getTag(node)[0].toUpperCase() + getTag(node).slice(1);\n}\n\n/**\n * @param {Node} node\n * @returns {boolean}\n */\nfunction isComment(node) {\n    return node.nodeType === 8;\n}\n\n/**\n * @param {Element} el\n * @returns {boolean}\n */\nexport function isComponentNode(el) {\n    return (\n        getTag(el) === getTitleTag(el) ||\n        (getTag(el, true) === \"t\" && \"t-component\" in el.attributes)\n    );\n}\n\n/**\n * @param {Node} node\n * @returns {boolean}\n */\nexport function isTextNode(node) {\n    return node.nodeType === 3;\n}\n\n/**\n * @param {string} title\n * @returns {Element}\n */\nexport function makeSeparator(title) {\n    const separator = createElement(\"div\");\n    separator.className = \"o_horizontal_separator mt-4 mb-3 text-uppercase fw-bolder small\";\n    separator.textContent = title;\n    return separator;\n}\n\nexport class ViewCompiler {\n    constructor(templates) {\n        /** @type {number} */\n        this.id = 1;\n        /** @type {Compiler[]} */\n        this.compilers = [\n            {\n                selector: \"a[type]:not([data-bs-toggle]),a[data-type]:not([data-bs-toggle])\",\n                fn: this.compileButton,\n            },\n            {\n                selector: \"button:not([data-bs-toggle])\",\n                fn: this.compileButton,\n                doNotCopyAttributes: true,\n            },\n            { selector: \"field\", fn: this.compileField },\n            { selector: \"widget\", fn: this.compileWidget },\n        ];\n        this.templates = templates;\n        this.ctx = { readonly: \"__comp__.props.readonly\" };\n\n        this.owlDirectiveRegexesWhitelist = this.constructor.OWL_DIRECTIVE_WHITELIST.map(\n            (d) => new RegExp(d)\n        );\n        this.setup();\n    }\n\n    setup() {}\n\n    /**\n     * @param {any} invisible\n     * @param {Element} compiled\n     * @param {Record<string, any>} params\n     * @returns {Element}\n     */\n    applyInvisible(invisible, compiled, params) {\n        if (!invisible || invisible === \"False\") {\n            return compiled;\n        }\n        if (invisible === \"True\" || invisible === \"1\") {\n            return;\n        }\n        const recordExpr = params.recordExpr || \"__comp__.props.record\";\n        let isVisileExpr = `!__comp__.evaluateBooleanExpr(${JSON.stringify(\n            invisible\n        )},${recordExpr}.evalContextWithVirtualIds)`;\n        if (compiled.hasAttribute(\"t-if\")) {\n            const formerTif = compiled.getAttribute(\"t-if\");\n            isVisileExpr = `( ${formerTif} ) and ${isVisileExpr}`;\n        }\n        compiled.setAttribute(\"t-if\", isVisileExpr);\n        return compiled;\n    }\n\n    /**\n     * @param {string} key\n     * @param {Record<string, any>} params\n     * @returns {string}\n     */\n    compile(key, params = {}) {\n        const root = this.templates[key].cloneNode(true);\n        const child = this.compileNode(root, params);\n        const newRoot = createElement(\"t\", [child]);\n        newRoot.setAttribute(\"t-translation\", \"off\");\n        return newRoot;\n    }\n\n    /**\n     * @param {Node} node\n     * @param {Record<string, any>} params\n     * @returns {Element | Text | void}\n     */\n    compileNode(node, params = {}, evalInvisible = true) {\n        if (isComment(node)) {\n            return;\n        }\n        if (isTextNode(node)) {\n            return createTextNode(node.nodeValue);\n        }\n\n        this.validateNode(node);\n        let invisible;\n        if (evalInvisible) {\n            invisible = getModifier(node, \"invisible\");\n            if (!params.compileInvisibleNodes && (invisible === \"True\" || invisible === \"1\")) {\n                return;\n            }\n        }\n\n        const compiler = this.compilers.find((cp) => node.matches(cp.selector));\n        let compiledNode;\n        if (compiler) {\n            compiledNode = compiler.fn.call(this, node, params);\n            if (!compiler.doNotCopyAttributes && compiledNode) {\n                copyAttributes(node, compiledNode);\n            }\n        } else {\n            compiledNode = this.compileGenericNode(node, params);\n        }\n\n        if (evalInvisible && compiledNode) {\n            compiledNode = this.applyInvisible(invisible, compiledNode, params);\n        }\n        return compiledNode;\n    }\n\n    //-----------------------------------------------------------------------------\n    // Compilers\n    //-----------------------------------------------------------------------------\n\n    /**\n     * @param {Element} el\n     * @param {Record<string, any>} params\n     * @returns {Element}\n     */\n    compileButton(el, params) {\n        let tag = getTag(el, true);\n        const type = el.getAttribute(\"type\");\n        if (tag === \"a\" && type === \"url\") {\n            tag = \"button\";\n        }\n        const recordExpr = params.recordExpr || \"__comp__.props.record\";\n        const button = createElement(\"ViewButton\", {\n            tag: toStringExpression(tag),\n            record: recordExpr,\n        });\n\n        assignOwlDirectives(button, el);\n\n        combineAttributes(\n            button,\n            \"className\",\n            [toStringExpression(el.className), button.className],\n            \"+` `+\"\n        );\n        el.removeAttribute(\"class\");\n        button.removeAttribute(\"class\");\n\n        const clickParams = {};\n        const attrs = {};\n        for (const { name, value } of el.attributes) {\n            if (BUTTON_CLICK_PARAMS.includes(name)) {\n                clickParams[name] = value;\n            } else if (BUTTON_STRING_PROPS.includes(name)) {\n                button.setAttribute(name, toStringExpression(value));\n            } else if (!name.startsWith(\"t-\")) {\n                attrs[name] = value;\n            }\n        }\n\n        button.setAttribute(\"clickParams\", JSON.stringify(clickParams));\n        button.setAttribute(\"attrs\", JSON.stringify(attrs));\n\n        // Button's body\n        const buttonContent = [];\n        for (const child of el.childNodes) {\n            const compiled = this.compileNode(child, params);\n            if (compiled) {\n                buttonContent.push(compiled);\n            }\n        }\n        if (buttonContent.length) {\n            const contentSlot = createElement(\"t\");\n            contentSlot.setAttribute(\"t-set-slot\", \"contents\");\n            append(button, contentSlot);\n            for (const buttonChild of buttonContent) {\n                append(contentSlot, buttonChild);\n            }\n        }\n        return button;\n    }\n\n    /**\n     * @param {Element} el\n     * @returns {Element}\n     */\n    compileField(el, params) {\n        const fieldName = el.getAttribute(\"name\");\n        const fieldId = el.getAttribute(\"field_id\");\n\n        const field = createElement(\"Field\");\n        const recordExpr = params.recordExpr || \"__comp__.props.record\";\n        field.setAttribute(\"id\", `'${fieldId}'`);\n        field.setAttribute(\"name\", `'${fieldName}'`);\n        field.setAttribute(\"record\", recordExpr);\n        field.setAttribute(\"fieldInfo\", `__comp__.props.archInfo.fieldNodes['${fieldId}']`);\n        field.setAttribute(\n            \"readonly\",\n            `__comp__.props.archInfo.activeActions?.edit === false and !${recordExpr}.isNew`\n        );\n\n        if (el.hasAttribute(\"widget\")) {\n            field.setAttribute(\"type\", `'${el.getAttribute(\"widget\")}'`);\n        }\n\n        return field;\n    }\n\n    /**\n     * @param {Element} el\n     * @param {Record<string, any>} params\n     * @returns {Element}\n     */\n    compileGenericNode(el, params) {\n        const compiled = createElement(el.nodeName.toLowerCase());\n        const metaAttrs = [\"column_invisible\", \"invisible\", \"readonly\", \"required\"];\n        for (const attr of el.attributes) {\n            if (metaAttrs.includes(attr.name)) {\n                continue;\n            }\n            compiled.setAttribute(attr.name, attr.value);\n        }\n        for (const child of el.childNodes) {\n            append(compiled, this.compileNode(child, params));\n        }\n        if (el.hasAttribute(\"t-foreach\") && !el.hasAttribute(\"t-key\")) {\n            compiled.setAttribute(\"t-key\", `${el.getAttribute(\"t-as\")}_index`);\n            console.warn(`Missing attribute \"t-key\" in \"t-foreach\" statement.`);\n        }\n        return compiled;\n    }\n\n    /**\n     * @param {Element} el\n     * @returns {Element}\n     */\n    compileWidget(el) {\n        const widgetId = el.getAttribute(\"widget_id\");\n        const props = { record: \"__comp__.props.record\" };\n        if (el.hasAttribute(\"name\")) {\n            props.name = `'${el.getAttribute(\"name\")}'`;\n        }\n        if (el.hasAttribute(\"class\")) {\n            props.className = `'${el.getAttribute(\"class\")}'`;\n        }\n        props.widgetInfo = `__comp__.props.archInfo.widgetNodes['${widgetId}']`;\n        const widget = createElement(\"Widget\", props);\n        return assignOwlDirectives(widget, el);\n    }\n\n    validateNode(node) {\n        // detect attributes not in whitelist, starting with t-\n        const attributes = Object.values(node.attributes).map((attr) => attr.name);\n        const regexes = this.owlDirectiveRegexesWhitelist;\n        for (const attr of attributes) {\n            if (attr.startsWith(\"t-\") && !regexes.some((regex) => regex.test(attr))) {\n                console.warn(`Forbidden directive ${attr} used in arch`);\n            }\n        }\n    }\n}\nViewCompiler.OWL_DIRECTIVE_WHITELIST = [];\n\nlet templateCache = Object.create(null);\n/**\n * @param {typeof ViewCompiler} ViewCompiler\n * @param {string} key\n * @param {Record<string, Element>} templates\n * @param {Record<string, any>} [params]\n * @returns {Record<string, string>}\n */\nexport function useViewCompiler(ViewCompiler, templates, params) {\n    const compiledTemplates = {};\n    let compiler;\n    for (const tname in templates) {\n        const key = `${ViewCompiler.name}/${templates[tname].outerHTML}`;\n        if (!templateCache[key]) {\n            compiler = compiler || new ViewCompiler(templates);\n            templateCache[key] = xml`${compiler.compile(tname, params).outerHTML}`;\n        }\n        compiledTemplates[tname] = templateCache[key];\n    }\n    return compiledTemplates;\n}\n\n/*\n * clear the view compiler's cache.\n * FIXME: that function only purges the compiler's cache and NOT the cache in owl's app.\n * the owl.xml function creates an internal template each time, so the cache is here to prevent\n * creating new owl templates every time. If we clear the cache, new templates WILL be created,\n * even if the arch to compile is the same.\n * This is how a memory leak occurs. :-)\n */\nexport function resetViewCompilerCache() {\n    templateCache = Object.create(null);\n}\n", "import { browser } from \"@web/core/browser/browser\";\nimport { formatInteger } from \"@web/views/fields/formatters\";\n\nimport { Component, onWillUpdateProps, onWillUnmount, useState } from \"@odoo/owl\";\n\nexport class AnimatedNumber extends Component {\n    static template = \"web.AnimatedNumber\";\n    static props = {\n        value: Number,\n        duration: Number,\n        animationClass: { type: String, optional: true },\n        currency: { type: [Object, Boolean], optional: true },\n        title: { type: String, optional: true },\n        slots: {\n            type: Object,\n            shape: {\n                prefix: { type: Object, optional: true },\n            },\n            optional: true,\n        },\n    };\n    static enableAnimations = true;\n\n    setup() {\n        this.formatInteger = formatInteger;\n        this.state = useState({ value: this.props.value });\n        this.handle = null;\n        onWillUpdateProps((nextProps) => {\n            const { value: from } = this.props;\n            const { value: to, duration } = nextProps;\n            if (!this.constructor.enableAnimations || !duration || to <= from) {\n                browser.cancelAnimationFrame(this.handle);\n                this.state.value = to;\n                return;\n            }\n            const startTime = Date.now();\n            const animate = () => {\n                const progress = (Date.now() - startTime) / duration;\n                if (progress >= 1) {\n                    this.state.value = to;\n                } else {\n                    this.state.value = from + (to - from) * progress;\n                    this.handle = browser.requestAnimationFrame(animate);\n                }\n            };\n            browser.cancelAnimationFrame(this.handle);\n            animate();\n        });\n        onWillUnmount(() => browser.cancelAnimationFrame(this.handle));\n    }\n\n    format(value) {\n        return this.formatInteger(value, { humanReadable: true, decimals: 0, minDigits: 3 });\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { AnimatedNumber } from \"./animated_number\";\n\nexport class ColumnProgress extends Component {\n    static components = {\n        AnimatedNumber,\n    };\n    static template = \"web.ColumnProgress\";\n    static props = {\n        aggregate: { type: Object },\n        group: { type: Object },\n        onBarClicked: { type: Function, optional: true },\n        progressBar: { type: Object },\n    };\n    static defaultProps = {\n        onBarClicked: () => {},\n    };\n\n    async onBarClick(bar) {\n        await this.props.onBarClicked(bar);\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\n\nexport class ReportViewMeasures extends Component {\n    static template = \"web.ReportViewMeasures\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n    };\n    static props = {\n        measures: true,\n        activeMeasures: { type: Array, optional: true },\n        onMeasureSelected: { type: Function, optional: true },\n    };\n}\n", "import { Component } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\n\nexport class ViewScaleSelector extends Component {\n    static components = {\n        Dropdown,\n        DropdownItem,\n    };\n    static template = \"web.ViewScaleSelector\";\n    static props = {\n        scales: { type: Object },\n        currentScale: { type: String },\n        isWeekendVisible: { type: Boolean, optional: true },\n        setScale: { type: Function },\n        toggleWeekendVisibility: { type: Function, optional: true },\n        dropdownClass: { type: String, optional: true },\n    };\n    get scales() {\n        return Object.entries(this.props.scales).map(([key, value]) => ({ key, ...value }));\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { unique } from \"@web/core/utils/arrays\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { fuzzyLookup } from \"@web/core/utils/search\";\nimport { useSortable } from \"@web/core/utils/sortable_owl\";\nimport { useDebounced } from \"@web/core/utils/timing\";\n\nimport { Component, useRef, useState, onMounted, onWillStart, onWillUnmount } from \"@odoo/owl\";\n\nclass DeleteExportListDialog extends Component {\n    static components = { Dialog };\n    static template = \"web.DeleteExportListDialog\";\n    static props = {\n        text: String,\n        close: Function,\n        delete: Function,\n    };\n    async onDelete() {\n        await this.props.delete();\n        this.props.close();\n    }\n}\n\nclass ExportDataItem extends Component {\n    static template = \"web.ExportDataItem\";\n    static components = { ExportDataItem };\n    static props = {\n        exportList: { type: Object, optional: true },\n        field: { type: Object, optional: true },\n        filterSubfields: Function,\n        isDebug: Boolean,\n        isExpanded: Boolean,\n        isFieldExpandable: Function,\n        onAdd: Function,\n        loadFields: Function,\n    };\n\n    setup() {\n        this.state = useState({\n            subfields: [],\n        });\n        onWillStart(() => {\n            if (this.props.isExpanded) {\n                // automatically expand the item when subfields are already loaded\n                // and display subfields that match the search string\n                return this.toggleItem(this.props.field.id, false);\n            }\n        });\n    }\n\n    async toggleItem(id, isUserToggle) {\n        if (this.props.isFieldExpandable(id)) {\n            if (this.state.subfields.length) {\n                this.state.subfields = [];\n            } else {\n                const subfields = await this.props.loadFields(id, !isUserToggle);\n                if (subfields) {\n                    this.state.subfields = isUserToggle\n                        ? subfields\n                        : this.props.filterSubfields(subfields);\n                } else {\n                    this.state.subfields = [];\n                }\n            }\n        }\n    }\n\n    onDoubleClick(id) {\n        if (!this.props.isFieldExpandable(id) && !this.isFieldSelected(id)) {\n            this.props.onAdd(id);\n        }\n    }\n\n    isFieldSelected(current) {\n        return this.props.exportList.find(({ id }) => id === current);\n    }\n}\n\nexport class ExportDataDialog extends Component {\n    static template = \"web.ExportDataDialog\";\n    static components = { CheckBox, Dialog, ExportDataItem };\n    static props = {\n        close: { type: Function },\n        context: { type: Object, optional: true },\n        defaultExportList: { type: Array },\n        download: { type: Function },\n        getExportedFields: { type: Function },\n        root: { type: Object },\n    };\n\n    setup() {\n        this.dialog = useService(\"dialog\");\n        this.notification = useService(\"notification\");\n        this.orm = useService(\"orm\");\n        this.draggableRef = useRef(\"draggable\");\n        this.exportListRef = useRef(\"exportList\");\n        this.searchRef = useRef(\"search\");\n\n        this.knownFields = {};\n        this.expandedFields = {};\n        this.availableFormats = [];\n        this.templates = [];\n\n        this.state = useState({\n            exportList: [],\n            isCompatible: false,\n            isEditingTemplate: false,\n            search: [],\n            selectedFormat: 0,\n            templateId: null,\n            isSmall: this.env.isSmall,\n            disabled: false,\n        });\n\n        this.newTemplateText = _t(\"New template\");\n        this.removeFieldText = _t(\"Remove field\");\n\n        this.debouncedOnResize = useDebounced(this.updateSize, 300);\n\n        useSortable({\n            // Params\n            ref: this.draggableRef,\n            elements: \".o_export_field\",\n            enable: !this.state.isSmall,\n            cursor: \"grabbing\",\n            // Hooks\n            onDrop: async ({ element, previous, next }) => {\n                const indexes = [element, previous, next].map(\n                    (e) =>\n                        e &&\n                        Object.values(this.state.exportList).findIndex(\n                            ({ id }) => id === e.dataset.field_id\n                        )\n                );\n                let target;\n                if (indexes[0] < indexes[1]) {\n                    target = previous ? indexes[1] : 0;\n                } else {\n                    target = next ? indexes[2] : this.state.exportList.length - 1;\n                }\n                this.onDraggingEnd(indexes[0], target);\n            },\n        });\n\n        onWillStart(async () => {\n            this.availableFormats = await rpc(\"/web/export/formats\");\n            this.templates = await this.orm.searchRead(\n                \"ir.exports\",\n                [[\"resource\", \"=\", this.props.root.resModel]],\n                [],\n                {\n                    context: this.props.context,\n                }\n            );\n            await this.fetchFields();\n        });\n\n        onMounted(() => {\n            browser.addEventListener(\"resize\", this.debouncedOnResize);\n            this.updateSize();\n        });\n\n        onWillUnmount(() => browser.removeEventListener(\"resize\", this.debouncedOnResize));\n    }\n\n    get fieldsAvailable() {\n        if (this.searchRef.el && this.searchRef.el.value) {\n            return this.state.search.length && Object.values(this.state.search);\n        }\n        return Object.values(this.knownFields);\n    }\n\n    get isDebug() {\n        return Boolean(odoo.debug);\n    }\n\n    get rootFields() {\n        if (this.searchRef.el && this.searchRef.el.value) {\n            const rootFromSearchResults = this.fieldsAvailable.map((f) => {\n                if (f.parent) {\n                    const parentEl = this.knownFields[f.parent.id];\n                    return this.knownFields[parentEl.parent ? parentEl.parent.id : parentEl.id];\n                }\n                return this.knownFields[f.id];\n            });\n            return unique(rootFromSearchResults);\n        }\n        return this.fieldsAvailable.filter(({ parent }) => !parent);\n    }\n\n    filterSubfields(subfields) {\n        let subfieldsFromSearchResults = [];\n        let searchResults;\n        if (this.searchRef.el && this.searchRef.el.value) {\n            searchResults = this.lookup(this.searchRef.el.value);\n        }\n        const fieldsAvailable = Object.values(searchResults || this.knownFields);\n        if (this.searchRef.el && this.searchRef.el.value) {\n            subfieldsFromSearchResults = fieldsAvailable\n                .filter((f) => f.parent && this.knownFields[f.parent.id].parent)\n                .map((f) => f.parent);\n        }\n        const availableSubFields = unique([...fieldsAvailable, ...subfieldsFromSearchResults]);\n        return subfields.filter((a) => availableSubFields.some((b) => a.id === b.id));\n    }\n\n    updateSize() {\n        this.state.isSmall = this.env.isSmall;\n    }\n\n    /**\n     * Load fields to display and (re)set the list of available fields\n     */\n    async fetchFields() {\n        this.state.search = [];\n        this.knownFields = {};\n        this.expandedFields = {};\n        await this.loadFields();\n        await this.setDefaultExportList();\n        if (this.searchRef.el) {\n            this.searchRef.el.value = \"\";\n        }\n        if (this.state.templateId) {\n            this.loadExportList(this.state.templateId);\n        }\n    }\n\n    enterTemplateEdition() {\n        if (this.state.templateId && !this.state.isEditingTemplate) {\n            this.state.isEditingTemplate = true;\n        }\n    }\n\n    isFieldExpandable(id) {\n        return this.knownFields[id].children && id.split(\"/\").length < 3;\n    }\n\n    async loadExportList(value) {\n        this.state.templateId = value === \"new_template\" ? value : Number(value);\n        this.state.isEditingTemplate = value === \"new_template\";\n        if (!value || value === \"new_template\") {\n            return;\n        }\n        const fields = await rpc(\"/web/export/namelist\", {\n            model: this.props.root.resModel,\n            export_id: Number(value),\n        });\n        // Don't safe the result in this.knownFields because, the result is only partial\n        this.state.exportList = fields;\n    }\n\n    async loadFields(id, preventLoad = false) {\n        let model = this.props.root.resModel;\n        let parentField, parentParams;\n        if (id) {\n            if (this.expandedFields[id]) {\n                // we don't make a new RPC if the value is already known\n                return this.expandedFields[id].fields;\n            }\n            parentField = this.knownFields[id];\n            model = parentField.params && parentField.params.model;\n            parentParams = {\n                ...parentField.params,\n                parent_field_type: parentField.field_type,\n                parent_field: parentField,\n                parent_name: parentField.string,\n                exclude: [parentField.relation_field],\n            };\n        }\n        if (preventLoad) {\n            return;\n        }\n        const fields = await this.props.getExportedFields(\n            model,\n            this.state.isCompatible,\n            parentParams\n        );\n        for (const field of fields) {\n            field.parent = parentField;\n            if (!this.knownFields[field.id]) {\n                this.knownFields[field.id] = field;\n            }\n        }\n        if (id) {\n            this.expandedFields[id] = { fields };\n        }\n        return fields;\n    }\n\n    onDraggingEnd(item, target) {\n        this.state.exportList.splice(target, 0, this.state.exportList.splice(item, 1)[0]);\n    }\n\n    onAddItemExportList(fieldId) {\n        this.state.exportList.push(this.knownFields[fieldId]);\n        this.enterTemplateEdition();\n    }\n\n    onRemoveItemExportList(fieldId) {\n        const item = this.state.exportList.findIndex(({ id }) => id === fieldId);\n        this.state.exportList.splice(item, 1);\n        this.enterTemplateEdition();\n    }\n\n    async onChangeExportList(ev) {\n        this.loadExportList(ev.target.value);\n    }\n\n    async onSaveExportTemplate() {\n        const name = this.exportListRef.el.value;\n        if (!name) {\n            return this.notification.add(_t(\"Please enter save field list name\"), {\n                type: \"danger\",\n            });\n        }\n        const [id] = await this.orm.create(\n            \"ir.exports\",\n            [\n                {\n                    name,\n                    export_fields: this.state.exportList.map((field) => [\n                        0,\n                        0,\n                        {\n                            name: field.id,\n                        },\n                    ]),\n                    resource: this.props.root.resModel,\n                },\n            ],\n            { context: this.props.context }\n        );\n        this.state.isEditingTemplate = false;\n        this.state.templateId = id;\n        this.templates.push({ id, name });\n    }\n\n    onCancelExportTemplate() {\n        this.state.isEditingTemplate = false;\n        if (this.state.templateId === \"new_template\") {\n            this.state.templateId = null;\n            return;\n        }\n        this.loadExportList(this.state.templateId);\n    }\n\n    async onClickExportButton() {\n        if (!this.state.exportList.length) {\n            return this.notification.add(_t(\"Please select fields to save export list...\"), {\n                type: \"danger\",\n            });\n        }\n        this.state.disabled = true;\n        await this.props.download(\n            this.state.exportList,\n            this.state.isCompatible,\n            this.availableFormats[this.state.selectedFormat].tag\n        );\n        this.state.disabled = false;\n    }\n\n    async onDeleteExportTemplate() {\n        this.dialog.add(DeleteExportListDialog, {\n            text: _t(\"Do you really want to delete this export template?\"),\n            delete: async () => {\n                const id = Number(this.state.templateId);\n                await this.orm.unlink(\"ir.exports\", [id], { context: this.props.context });\n                this.templates.splice(\n                    this.templates.findIndex((i) => i.id === id),\n                    1\n                );\n                this.state.templateId = null;\n                this.setDefaultExportList();\n            },\n        });\n    }\n\n    onSearch(ev) {\n        this.state.search = this.lookup(ev.target.value);\n    }\n\n    lookup(value) {\n        let lookupResult = fuzzyLookup(\n            value,\n            Object.values(this.knownFields),\n            // because fuzzyLookup gives an higher score if the string starts with the pattern,\n            // reversing the string makes the search more reliable in this context\n            (field) => field.string.split(\"/\").reverse().join(\"/\")\n        );\n        if (this.isDebug) {\n            lookupResult = unique([\n                ...lookupResult,\n                ...Object.values(this.knownFields).filter((f) => {\n                    return f.id.includes(value);\n                }),\n            ]);\n        }\n        return lookupResult;\n    }\n\n    onToggleCompatibleExport(value) {\n        this.state.isCompatible = value;\n        this.fetchFields();\n    }\n\n    async setDefaultExportList() {\n        this.state.exportList = Object.values(this.knownFields).filter(\n            (e) => e.default_export || this.props.defaultExportList.find((i) => i.name === e.id)\n        );\n    }\n\n    setFormat(ev) {\n        if (ev.target.checked) {\n            this.state.selectedFormat = this.availableFormats.findIndex(\n                ({ tag }) => tag === ev.target.value\n            );\n        }\n    }\n}\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { useChildRef, useService } from \"@web/core/utils/hooks\";\nimport { CallbackRecorder } from \"@web/search/action_hook\";\nimport { View } from \"@web/views/view\";\n\nimport { Component, onMounted } from \"@odoo/owl\";\n\nexport class FormViewDialog extends Component {\n    static template = \"web.FormViewDialog\";\n    static components = { Dialog, View };\n    static props = {\n        close: Function,\n        resModel: String,\n\n        context: { type: Object, optional: true },\n        nextRecordsContext: { type: Object, optional: true },\n        mode: {\n            optional: true,\n            validate: (m) => [\"edit\", \"readonly\"].includes(m),\n        },\n        onRecordSaved: { type: Function, optional: true },\n        onRecordDiscarded: { type: Function, optional: true },\n        removeRecord: { type: Function, optional: true },\n        resId: { type: [Number, Boolean], optional: true },\n        title: { type: String, optional: true },\n        viewId: { type: [Number, Boolean], optional: true },\n        preventCreate: { type: Boolean, optional: true },\n        preventEdit: { type: Boolean, optional: true },\n        isToMany: { type: Boolean, optional: true },\n        size: Dialog.props.size,\n    };\n    static defaultProps = {\n        onRecordSaved: () => {},\n        preventCreate: false,\n        preventEdit: false,\n        isToMany: false,\n    };\n\n    setup() {\n        super.setup();\n\n        this.actionService = useService(\"action\");\n        this.modalRef = useChildRef();\n        this.env.dialogData.dismiss = () => this.discardRecord();\n\n        const buttonTemplate = this.props.isToMany\n            ? \"web.FormViewDialog.ToMany.buttons\"\n            : \"web.FormViewDialog.ToOne.buttons\";\n\n        this.currentResId = this.props.resId;\n\n        this.viewProps = {\n            type: \"form\",\n            buttonTemplate,\n\n            context: this.props.context || {},\n            display: { controlPanel: false },\n            mode: this.props.mode || \"edit\",\n            resId: this.props.resId || false,\n            resModel: this.props.resModel,\n            viewId: this.props.viewId || false,\n            preventCreate: this.props.preventCreate,\n            preventEdit: this.props.preventEdit,\n            discardRecord: this.discardRecord.bind(this),\n            saveRecord: async (record, { saveAndNew }) => {\n                const saved = await record.save({ reload: false });\n                if (saved) {\n                    this.currentResId = record.resId;\n                    await this.props.onRecordSaved(record);\n                    if (saveAndNew) {\n                        this.currentResId = false;\n                        const context = this.props.nextRecordsContext || this.props.context || {};\n                        await record.model.load({ resId: false, context });\n                    } else {\n                        this.props.close();\n                    }\n                }\n                return saved;\n            },\n\n            __beforeLeave__: new CallbackRecorder(),\n        };\n        if (this.props.removeRecord) {\n            this.viewProps.removeRecord = async () => {\n                await this.props.removeRecord();\n                this.props.close();\n            };\n        }\n\n        onMounted(() => {\n            if (this.modalRef.el.querySelector(\".modal-footer\").childElementCount > 1) {\n                const defaultButton = this.modalRef.el.querySelector(\n                    \".modal-footer button.o-default-button\"\n                );\n                if (defaultButton) {\n                    defaultButton.classList.add(\"d-none\");\n                }\n            }\n        });\n    }\n\n    async discardRecord() {\n        if (this.props.onRecordDiscarded) {\n            await this.props.onRecordDiscarded();\n        }\n        this.props.close();\n    }\n\n    async onExpand() {\n        const beforeLeaveCallbacks = this.viewProps.__beforeLeave__.callbacks;\n        const res = await Promise.all(beforeLeaveCallbacks.map((callback) => callback()));\n        if (!res.includes(false)) {\n            this.actionService.doAction({\n                type: \"ir.actions.act_window\",\n                res_model: this.props.resModel,\n                res_id: this.currentResId,\n                views: [[false, \"form\"]],\n            });\n        }\n    }\n}\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { renderToMarkup } from \"@web/core/utils/render\";\nimport { View } from \"@web/views/view\";\n\nimport { FormViewDialog } from \"./form_view_dialog\";\n\nimport { Component, useState } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\n\nlet _defaultNoContentHelp;\nfunction getDefaultNoContentHelp() {\n    if (!_defaultNoContentHelp) {\n        _defaultNoContentHelp = renderToMarkup(\"web.SelectCreateDialog.DefaultNoContentHelp\");\n    }\n    return _defaultNoContentHelp;\n}\n\nexport class SelectCreateDialog extends Component {\n    static components = { Dialog, View };\n    static template = \"web.SelectCreateDialog\";\n    static props = {\n        context: { type: Object, optional: true },\n        domain: { type: Array, optional: true },\n        dynamicFilters: { type: Array, optional: true },\n        resModel: String,\n        searchViewId: { type: [Number, { value: false }], optional: true },\n        multiSelect: { type: Boolean, optional: true },\n        onSelected: { type: Function, optional: true },\n        close: { type: Function, optional: true },\n        onCreateEdit: { type: Function, optional: true },\n        title: { type: String, optional: true },\n        noCreate: { type: Boolean, optional: true },\n        onUnselect: { type: Function, optional: true },\n        noContentHelp: { type: String, optional: true }, // Markup\n    };\n    static defaultProps = {\n        dynamicFilters: [],\n        multiSelect: true,\n        searchViewId: false,\n        domain: [],\n        context: {},\n    };\n\n    setup() {\n        this.viewService = useService(\"view\");\n        this.dialogService = useService(\"dialog\");\n        this.state = useState({ resIds: [] });\n        const noContentHelp = this.props.noContentHelp || getDefaultNoContentHelp();\n        this.busy = false; // flag used to ensure we only call once the onSelected/onUnselect props\n        this.baseViewProps = {\n            display: { searchPanel: false },\n            editable: false, // readonly\n            noBreadcrumbs: true,\n            noContentHelp,\n            showButtons: false,\n            selectRecord: (resId) => this.select([resId]),\n            onSelectionChanged: (resIds) => {\n                this.state.resIds = resIds;\n            },\n        };\n    }\n\n    get viewProps() {\n        const type = this.env.isSmall ? \"kanban\" : \"list\";\n        const props = {\n            loadIrFilters: true,\n            ...this.baseViewProps,\n            context: this.props.context,\n            domain: this.props.domain,\n            dynamicFilters: this.props.dynamicFilters,\n            resModel: this.props.resModel,\n            searchViewId: this.props.searchViewId,\n            type,\n        };\n        if (type === \"list\") {\n            props.allowSelectors = this.props.multiSelect;\n        } else if (type === \"kanban\") {\n            props.forceGlobalClick = true;\n        }\n        return props;\n    }\n\n    async executeOnceAndClose(callback) {\n        if (!this.busy) {\n            this.busy = true;\n            try {\n                await callback();\n            } catch (e) {\n                this.busy = false;\n                throw e;\n            }\n            this.props.close();\n        }\n    }\n\n    async select(resIds) {\n        if (this.props.onSelected) {\n            this.executeOnceAndClose(() => this.props.onSelected(resIds));\n        }\n    }\n\n    async unselect() {\n        if (this.props.onUnselect) {\n            this.executeOnceAndClose(() => this.props.onUnselect());\n        }\n    }\n\n    get canUnselect() {\n        return this.env.isSmall && !!this.props.onUnselect;\n    }\n\n    async createEditRecord() {\n        if (this.props.onCreateEdit) {\n            await this.props.onCreateEdit();\n            this.props.close();\n        } else {\n            this.dialogService.add(FormViewDialog, {\n                context: this.props.context,\n                resModel: this.props.resModel,\n                onRecordSaved: (record) => {\n                    this.props.onSelected([record.resId]);\n                    this.props.close();\n                },\n            });\n        }\n    }\n}\n\nregistry.category(\"dialogs\").add(\"select_create\", SelectCreateDialog);\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { evaluateExpr } from \"@web/core/py_js/py\";\n\nimport { useComponent, useEffect, xml } from \"@odoo/owl\";\n\nexport function useViewArch(arch, params = {}) {\n    const CATEGORY = \"__processed_archs__\";\n\n    arch = arch.trim();\n    const processedRegistry = registry.category(CATEGORY);\n\n    let processedArch;\n    if (!processedRegistry.contains(arch)) {\n        processedArch = {};\n        processedRegistry.add(arch, processedArch);\n    } else {\n        processedArch = processedRegistry.get(arch);\n    }\n\n    const { compile, extract } = params;\n    if (!(\"template\" in processedArch) && compile) {\n        processedArch.template = xml`${compile(arch)}`;\n    }\n    if (!(\"extracted\" in processedArch) && extract) {\n        processedArch.extracted = extract(arch);\n    }\n\n    return processedArch;\n}\n\n/**\n * Allows for a component (usually a View component) to handle links with\n * attribute type=\"action\". This is used to support onboarding banners and content helpers.\n *\n * A @web/core/concurrency:KeepLast must be present in the owl environment to allow coordinating\n * between clicks. (env.keepLast)\n *\n * Note that this is similar but quite different from action buttons, since action links\n * are not dynamic according to the record.\n * @param {Object} params\n * @param  {String} params.resModel The default resModel to which actions will apply\n * @param  {Function} [params.reload] The function to execute to reload, if a button has data-reload-on-close\n */\nexport function useActionLinks({ resModel, reload }) {\n    const component = useComponent();\n    const keepLast = component.env.keepLast;\n\n    const orm = useService(\"orm\");\n    const { doAction } = useService(\"action\");\n\n    async function handler(ev) {\n        ev.preventDefault();\n        ev.stopPropagation();\n        let target = ev.target;\n        if (target.tagName !== \"A\") {\n            target = target.closest(\"a\");\n        }\n        const data = target.dataset;\n\n        if (data.method !== undefined && data.model !== undefined) {\n            const options = {};\n            if (data.reloadOnClose) {\n                options.onClose = reload || (() => component.render());\n            }\n            const action = await keepLast.add(orm.call(data.model, data.method));\n            if (action !== undefined) {\n                keepLast.add(Promise.resolve(doAction(action, options)));\n            }\n        } else if (target.getAttribute(\"name\")) {\n            const options = {};\n            if (data.context) {\n                options.additionalContext = evaluateExpr(data.context);\n            }\n            keepLast.add(doAction(target.getAttribute(\"name\"), options));\n        } else {\n            let views;\n            const resId = data.resid ? parseInt(data.resid, 10) : null;\n            if (data.views) {\n                views = evaluateExpr(data.views);\n            } else {\n                views = resId\n                    ? [[false, \"form\"]]\n                    : [\n                          [false, \"list\"],\n                          [false, \"form\"],\n                      ];\n            }\n            const action = {\n                name: target.getAttribute(\"title\") || target.textContent.trim(),\n                type: \"ir.actions.act_window\",\n                res_model: data.model || resModel,\n                target: \"current\",\n                views,\n                domain: data.domain ? evaluateExpr(data.domain) : [],\n            };\n            if (resId) {\n                action.res_id = resId;\n            }\n\n            const options = {};\n            if (data.context) {\n                options.additionalContext = evaluateExpr(data.context);\n            }\n            keepLast.add(doAction(action, options));\n        }\n    }\n\n    return (ev) => {\n        const a = ev.target.closest(`a[type=\"action\"]`);\n        if (a && ev.currentTarget.contains(a)) {\n            handler(ev);\n        }\n    };\n}\n\nexport function useBounceButton(containerRef, shouldBounce) {\n    let timeout;\n    const ui = useService(\"ui\");\n    useEffect(\n        (containerEl) => {\n            if (!containerEl) {\n                return;\n            }\n            const handler = (ev) => {\n                const button = ui.activeElement.querySelector(\"[data-bounce-button]\");\n                if (button && shouldBounce(ev.target)) {\n                    button.classList.add(\"o_catch_attention\");\n                    browser.clearTimeout(timeout);\n                    timeout = browser.setTimeout(() => {\n                        button.classList.remove(\"o_catch_attention\");\n                    }, 400);\n                }\n            };\n            containerEl.addEventListener(\"click\", handler);\n            return () => containerEl.removeEventListener(\"click\", handler);\n        },\n        () => [containerRef.el]\n    );\n}\n", "import { rpcBus } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { UPDATE_METHODS } from \"@web/core/orm_service\";\n\n/**\n * @typedef {Object} IrFilter\n * @property {[number, string] | false} user_id\n * @property {string} sort\n * @property {string} context\n * @property {string} name\n * @property {string} domain\n * @property {number} id\n * @property {boolean} is_default\n * @property {string} model_id\n * @property {[number, string] | false} action_id\n * @property {number | false} embedded_action_id\n * @property {number | false} embedded_parent_res_id\n */\n\n/**\n * @typedef {Object} ViewDescription\n * @property {string} arch\n * @property {number|false} id\n * @property {number|null} [custom_view_id]\n * @property {Object} [actionMenus] // for views other than search\n * @property {IrFilter[]} [irFilters] // for search view\n */\n\n/**\n * @typedef {Object} LoadViewsParams\n * @property {string} resModel\n * @property {[number, string][]} views\n * @property {Object} context\n */\n\n/**\n * @typedef {Object} LoadViewsOptions\n * @property {number|false} actionId\n * @property {boolean} loadActionMenus\n * @property {boolean} loadIrFilters\n */\n\nexport const viewService = {\n    dependencies: [\"orm\"],\n    async: [\"loadViews\"],\n    start(env, { orm }) {\n        let cache = {};\n\n        function clearCache() {\n            cache = {};\n            const processedArchs = registry.category(\"__processed_archs__\");\n            processedArchs.content = {};\n            processedArchs.trigger(\"UPDATE\");\n        }\n\n        env.bus.addEventListener(\"CLEAR-CACHES\", clearCache);\n        rpcBus.addEventListener(\"RPC:RESPONSE\", (ev) => {\n            const { model, method } = ev.detail.data.params;\n            if ([\"ir.ui.view\", \"ir.filters\"].includes(model)) {\n                if (UPDATE_METHODS.includes(method)) {\n                    clearCache();\n                }\n            }\n        });\n\n        /**\n         * Loads various information concerning views: fields_view for each view,\n         * fields of the corresponding model, and optionally the filters.\n         *\n         * @param {LoadViewsParams} params\n         * @param {LoadViewsOptions} [options={}]\n         * @returns {Promise<ViewDescriptions>}\n         */\n        async function loadViews(params, options = {}) {\n            const { context, resModel, views } = params;\n            const loadViewsOptions = {\n                action_id: options.actionId || false,\n                embedded_action_id: options.embeddedActionId || false,\n                embedded_parent_res_id: options.embeddedParentResId || false,\n                load_filters: options.loadIrFilters || false,\n                toolbar: (!context?.disable_toolbar && options.loadActionMenus) || false,\n            };\n            for (const key in options) {\n                if (\n                    ![\n                        \"actionId\",\n                        \"embeddedActionId\",\n                        \"embeddedParentResId\",\n                        \"loadIrFilters\",\n                        \"loadActionMenus\",\n                    ].includes(key)\n                ) {\n                    loadViewsOptions[key] = options[key];\n                }\n            }\n            if (env.isSmall) {\n                loadViewsOptions.mobile = true;\n            }\n            const filteredContext = Object.fromEntries(\n                Object.entries(context || {}).filter(\n                    ([k, v]) => k == \"lang\" || k.endsWith(\"_view_ref\")\n                )\n            );\n\n            const key = JSON.stringify([resModel, views, filteredContext, loadViewsOptions]);\n            if (!cache[key]) {\n                cache[key] = orm\n                    .call(resModel, \"get_views\", [], {\n                        context: filteredContext,\n                        views,\n                        options: loadViewsOptions,\n                    })\n                    .then((result) => {\n                        const { models, views } = result;\n                        const viewDescriptions = {\n                            fields: models[resModel].fields,\n                            relatedModels: models,\n                            views: {},\n                        };\n                        for (const viewType in views) {\n                            const { arch, toolbar, id, filters, custom_view_id } = views[viewType];\n                            const viewDescription = { arch, id, custom_view_id };\n                            if (toolbar) {\n                                viewDescription.actionMenus = toolbar;\n                            }\n                            if (filters) {\n                                viewDescription.irFilters = filters;\n                            }\n                            viewDescriptions.views[viewType] = viewDescription;\n                        }\n                        return viewDescriptions;\n                    })\n                    .catch((error) => {\n                        delete cache[key];\n                        return Promise.reject(error);\n                    });\n            }\n            return cache[key];\n        }\n        return { loadViews };\n    },\n};\n\nregistry.category(\"services\").add(\"view\", viewService);\n", "import { FileInput } from \"@web/core/file_input/file_input\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { checkFileSize } from \"@web/core/utils/files\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { Component } from \"@odoo/owl\";\n\nexport class AttachDocumentWidget extends Component {\n    static template = \"web.AttachDocument\";\n    static components = {\n        FileInput,\n    };\n    static props = {\n        ...standardWidgetProps,\n        string: { type: String },\n        action: { type: String, optional: true },\n        highlight: { type: Boolean },\n    };\n\n    setup() {\n        this.http = useService(\"http\");\n        this.notification = useService(\"notification\");\n        this.fileInput = document.createElement(\"input\");\n        this.fileInput.type = \"file\";\n        this.fileInput.accept = \"*\";\n        this.fileInput.multiple = true;\n        this.fileInput.onchange = this.onInputChange.bind(this);\n    }\n\n    async onInputChange() {\n        const ufile = [...this.fileInput.files];\n        for (const file of ufile) {\n            if (!checkFileSize(file.size, this.notification)) {\n                return null;\n            }\n        }\n        const fileData = await this.http.post(\n            \"/web/binary/upload_attachment\",\n            {\n                csrf_token: odoo.csrf_token,\n                ufile: ufile,\n                model: this.props.record.resModel,\n                id: this.props.record.resId,\n            },\n            \"text\"\n        );\n        const parsedFileData = JSON.parse(fileData);\n        if (parsedFileData.error) {\n            throw new Error(parsedFileData.error);\n        }\n        await this.onFileUploaded(parsedFileData);\n    }\n\n    async triggerUpload() {\n        if (await this.beforeOpen()) {\n            this.fileInput.click();\n        }\n    }\n\n    async onFileUploaded(files) {\n        const { action, record } = this.props;\n        if (action) {\n            const { resId, resModel } = record;\n            await this.env.services.orm.call(resModel, action, [resId], {\n                attachment_ids: files.map((file) => file.id),\n            });\n            await record.load();\n        }\n    }\n\n    beforeOpen() {\n        return this.props.record.save();\n    }\n}\n\nexport const attachDocumentWidget = {\n    component: AttachDocumentWidget,\n    extractProps: ({ attrs }) => {\n        const { action, highlight, string } = attrs;\n        return {\n            action,\n            highlight: !!highlight,\n            string,\n        };\n    },\n};\n\nregistry.category(\"view_widgets\").add(\"attach_document\", attachDocumentWidget);\n", "import { session } from \"@web/session\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { Component } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\n\nconst LINK_REGEX = new RegExp(\"^https?://\");\n\nexport class DocumentationLink extends Component {\n    static template = \"web.DocumentationLink\";\n    static props = {\n        ...standardWidgetProps,\n        record: { type: Object, optional: 1 }, // The record is not needed in this widget\n        path: { type: String },\n        label: { type: String, optional: 1 },\n        icon: { type: String, optional: 1 },\n        alertLink: { type: Boolean, optional: 1 },\n    };\n\n    get url() {\n        if (LINK_REGEX.test(this.props.path)) {\n            return this.props.path;\n        } else {\n            const serverVersion = session.server_version_info.includes(\"final\")\n                ? `${session.server_version_info[0]}.${session.server_version_info[1]}`.replace(\n                      \"~\",\n                      \"-\"\n                  )\n                : \"master\";\n            return \"https://www.odoo.com/documentation/\" + serverVersion + this.props.path;\n        }\n    }\n\n    get classes() {\n        let classes = \"o_doc_link me-2\";\n        if (this.props.alertLink){\n            classes += \" alert-link\";\n        }\n        return classes;\n    }\n}\n\nexport const documentationLink = {\n    component: DocumentationLink,\n    extractProps: ({ attrs }) => {\n        const { path, label, icon, alert_link } = attrs;\n        return {\n            path,\n            label,\n            icon,\n            alertLink: Boolean(alert_link),\n        };\n    },\n    additionalClasses: [\"d-inline\"],\n};\n\nregistry.category(\"view_widgets\").add(\"documentation_link\", documentationLink);\n", "import { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"@web/core/registry\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class NotificationAlert extends Component {\n    static props = standardWidgetProps;\n    static template = \"web.NotificationAlert\";\n\n    get isNotificationBlocked() {\n        return browser.Notification && browser.Notification.permission === \"denied\";\n    }\n}\n\nexport const notificationAlert = {\n    component: NotificationAlert,\n};\n\nregistry.category(\"view_widgets\").add(\"notification_alert\", notificationAlert);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { standardWidgetProps } from \"../standard_widget_props\";\n\nimport { Component } from \"@odoo/owl\";\n\n/**\n * This widget adds a ribbon on the top right side of the form\n *\n *      - You can specify the text with the title prop.\n *      - You can specify the title (tooltip) with the tooltip prop.\n *      - You can specify a background color for the ribbon with the bg_color prop\n *        using bootstrap classes :\n *        (bg-primary, bg-secondary, bg-success, bg-danger, bg-warning, bg-info,\n *        bg-light, bg-dark, bg-white)\n *\n *        If you don't specify the bg_color prop the bg-success class will be used\n *        by default.\n */\nclass RibbonWidget extends Component {\n    static template = \"web.Ribbon\";\n    static props = {\n        ...standardWidgetProps,\n        text: { type: String },\n        title: { type: String, optional: true },\n        bgClass: { type: String, optional: true },\n    };\n    static defaultProps = {\n        title: \"\",\n        bgClass: \"text-bg-success\",\n    };\n\n    get classes() {\n        let classes = this.props.bgClass;\n        if (this.props.text.length > 15) {\n            classes += \" o_small\";\n        } else if (this.props.text.length > 10) {\n            classes += \" o_medium\";\n        }\n        return classes;\n    }\n}\n\nexport const ribbonWidget = {\n    component: RibbonWidget,\n    extractProps: ({ attrs }) => {\n        return {\n            text: attrs.title || attrs.text,\n            title: attrs.tooltip,\n            bgClass: attrs.bg_color,\n        };\n    },\n    supportedAttributes: [\n        {\n            label: _t(\"Title\"),\n            name: \"title\",\n            type: \"string\",\n        },\n        {\n            label: _t(\"Background color\"),\n            name: \"bg_color\",\n            type: \"string\",\n        },\n        {\n            label: _t(\"Tooltip\"),\n            name: \"tooltip\",\n            type: \"string\",\n        },\n    ],\n};\n\nregistry.category(\"view_widgets\").add(\"web_ribbon\", ribbonWidget);\n", "import { registry } from \"@web/core/registry\";\nimport { SignatureDialog } from \"@web/core/signature/signature_dialog\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class SignatureWidget extends Component {\n    static template = \"web.SignatureWidget\";\n    static props = {\n        ...standardWidgetProps,\n        fullName: { type: String, optional: true },\n        highlight: { type: Boolean, optional: true },\n        string: { type: String },\n        signatureField: { type: String, optional: true },\n    };\n\n    setup() {\n        this.dialogService = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n    }\n\n    onClickSignature() {\n        const nameAndSignatureProps = {\n            mode: \"draw\",\n            displaySignatureRatio: 3,\n            signatureType: \"signature\",\n            noInputName: true,\n        };\n        const { fullName, record } = this.props;\n        let defaultName = \"\";\n        if (fullName) {\n            let signName;\n            const fullNameData = record.data[fullName];\n            if (record.fields[fullName].type === \"many2one\") {\n                signName = fullNameData && fullNameData[1];\n            } else {\n                signName = fullNameData;\n            }\n            defaultName = signName === \"\" ? undefined : signName;\n        }\n\n        nameAndSignatureProps.defaultFont = this.props.defaultFont;\n\n        const dialogProps = {\n            defaultName,\n            nameAndSignatureProps,\n            uploadSignature: (data) => this.uploadSignature(data),\n        };\n        this.dialogService.add(SignatureDialog, dialogProps);\n    }\n\n    async uploadSignature({ signatureImage }) {\n        const file = signatureImage.split(\",\")[1];\n        const { model, resModel, resId } = this.props.record;\n\n        await this.env.services.orm.write(resModel, [resId], {\n            [this.props.signatureField]: file,\n        });\n        await this.props.record.load();\n        model.notify();\n    }\n}\n\nexport const signatureWidget = {\n    component: SignatureWidget,\n    extractProps: ({ attrs }) => {\n        const { full_name: fullName, highlight, signature_field, string } = attrs;\n        return {\n            fullName,\n            highlight: !!highlight,\n            string,\n            signatureField: signature_field || \"signature\",\n        };\n    },\n};\n\nregistry.category(\"view_widgets\").add(\"signature\", signatureWidget);\n", "export const standardWidgetProps = {\n    readonly: { type: Boolean, optional: true },\n    record: { type: Object },\n};\n", "import { registry } from \"@web/core/registry\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport { Component } from \"@odoo/owl\";\n\nconst WEEKDAYS = [\"sun\", \"mon\", \"tue\", \"wed\", \"thu\", \"fri\", \"sat\"];\n\nexport class WeekDays extends Component {\n    static template = \"web.WeekDays\";\n    static components = { CheckBox };\n    static props = {\n        record: Object,\n        readonly: Boolean,\n    };\n\n    get weekdays() {\n        return [\n            ...WEEKDAYS.slice(localization.weekStart % WEEKDAYS.length, WEEKDAYS.length),\n            ...WEEKDAYS.slice(0, localization.weekStart % WEEKDAYS.length),\n        ];\n    }\n    get data() {\n        return Object.fromEntries(this.weekdays.map((day) => [day, this.props.record.data[day]]));\n    }\n\n    onChange(day, checked) {\n        this.props.record.update({ [day]: checked });\n    }\n}\n\nexport const weekDays = {\n    component: WeekDays,\n    fieldDependencies: [\n        { name: \"sun\", type: \"boolean\", string: _t(\"Sun\"), readonly: false },\n        { name: \"mon\", type: \"boolean\", string: _t(\"Mon\"), readonly: false },\n        { name: \"tue\", type: \"boolean\", string: _t(\"Tue\"), readonly: false },\n        { name: \"wed\", type: \"boolean\", string: _t(\"Wed\"), readonly: false },\n        { name: \"thu\", type: \"boolean\", string: _t(\"Thu\"), readonly: false },\n        { name: \"fri\", type: \"boolean\", string: _t(\"Fri\"), readonly: false },\n        { name: \"sat\", type: \"boolean\", string: _t(\"Sat\"), readonly: false },\n    ],\n};\n\nregistry.category(\"view_widgets\").add(\"week_days\", weekDays);\n", "import { evaluateExpr, evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { registry } from \"@web/core/registry\";\n\nimport { Component, xml } from \"@odoo/owl\";\nconst viewWidgetRegistry = registry.category(\"view_widgets\");\n\nconst supportedInfoValidation = {\n    type: Array,\n    element: Object,\n    shape: {\n        label: String,\n        name: String,\n        type: String,\n        availableTypes: { type: Array, element: String, optional: true },\n        default: { type: String, optional: true },\n        help: { type: String, optional: true },\n        choices: /* choices if type == selection */ {\n            type: Array,\n            element: Object,\n            shape: { label: String, value: String },\n            optional: true,\n        },\n    },\n    optional: true,\n};\n\nviewWidgetRegistry.addValidation({\n    component: { validate: (c) => c.prototype instanceof Component },\n    extractProps: { type: Function, optional: true },\n    additionalClasses: { type: Array, element: String, optional: true },\n    fieldDependencies: {\n        type: [Function, { type: Array, element: Object, shape: { name: String, type: String } }],\n        optional: true,\n    },\n    listViewWidth: {\n        type: [\n            Number,\n            {\n                type: Array,\n                element: Number,\n                validate: (array) => array.length === 1 || array.length === 2,\n            },\n            Function,\n        ],\n        optional: true,\n    },\n    supportedAttributes: supportedInfoValidation,\n    supportedOptions: supportedInfoValidation,\n});\n\n/**\n * A Component that supports rendering `<widget />` tags in a view arch\n * It should have minimum legacy support that is:\n * - getting the legacy widget class from the legacy registry\n * - instanciating a legacy widget\n * - passing to it a \"legacy node\", which is a representation of the arch's node\n * It supports instancing components from the \"view_widgets\" registry.\n */\nexport class Widget extends Component {\n    static template = xml/*xml*/ `\n        <div t-att-class=\"classNames\" t-att-style=\"props.style\">\n            <t t-component=\"widget.component\" t-props=\"widgetProps\" />\n        </div>`;\n\n    static parseWidgetNode = function (node) {\n        const name = node.getAttribute(\"name\");\n        const widget = viewWidgetRegistry.get(name);\n        const widgetInfo = {\n            name,\n            widget,\n            options: {},\n            attrs: {},\n        };\n\n        for (const { name, value } of node.attributes) {\n            if ([\"name\", \"widget\"].includes(name)) {\n                // avoid adding name and widget to attrs\n                continue;\n            }\n            if (name === \"options\") {\n                widgetInfo.options = evaluateExpr(value);\n            } else if (!name.startsWith(\"t-att\")) {\n                // all other (non dynamic) attributes\n                widgetInfo.attrs[name] = value;\n            }\n        }\n\n        return widgetInfo;\n    };\n    static props = {\n        \"*\": true,\n    };\n\n    setup() {\n        if (this.props.widgetInfo) {\n            this.widget = this.props.widgetInfo.widget;\n        } else {\n            this.widget = viewWidgetRegistry.get(this.props.name);\n        }\n    }\n\n    get classNames() {\n        const classNames = {\n            o_widget: true,\n            [`o_widget_${this.props.name}`]: true,\n            [this.props.className]: Boolean(this.props.className),\n        };\n        if (this.widget.additionalClasses) {\n            for (const cls of this.widget.additionalClasses) {\n                classNames[cls] = true;\n            }\n        }\n        return classNames;\n    }\n    get widgetProps() {\n        const record = this.props.record;\n\n        let readonlyFromModifiers = false;\n        let propsFromNode = {};\n        if (this.props.widgetInfo) {\n            const widgetInfo = this.props.widgetInfo;\n            readonlyFromModifiers = evaluateBooleanExpr(\n                widgetInfo.attrs.readonly,\n                record.evalContextWithVirtualIds\n            );\n            const dynamicInfo = {\n                readonly: readonlyFromModifiers,\n            };\n            propsFromNode = this.widget.extractProps\n                ? this.widget.extractProps(widgetInfo, dynamicInfo)\n                : {};\n        }\n\n        return {\n            record,\n            readonly: !record.isInEdition || readonlyFromModifiers || false,\n            ...propsFromNode,\n        };\n    }\n}\n", "import { ActionDialog } from \"./action_dialog\";\n\nimport { Component, xml, onWillDestroy } from \"@odoo/owl\";\n\n// -----------------------------------------------------------------------------\n// ActionContainer (Component)\n// -----------------------------------------------------------------------------\nexport class ActionContainer extends Component {\n    static components = { ActionDialog };\n    static props = {};\n    static template = xml`\n        <t t-name=\"web.ActionContainer\">\n          <div class=\"o_action_manager\">\n            <t t-if=\"info.Component\" t-component=\"info.Component\" className=\"'o_action'\" t-props=\"info.componentProps\" t-key=\"info.id\"/>\n          </div>\n        </t>`;\n\n    setup() {\n        this.info = {};\n        this.onActionManagerUpdate = ({ detail: info }) => {\n            this.info = info;\n            this.render();\n        };\n        this.env.bus.addEventListener(\"ACTION_MANAGER:UPDATE\", this.onActionManagerUpdate);\n        onWillDestroy(() => {\n            this.env.bus.removeEventListener(\"ACTION_MANAGER:UPDATE\", this.onActionManagerUpdate);\n        });\n    }\n}\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { DebugMenu } from \"@web/core/debug/debug_menu\";\nimport { useOwnDebugContext } from \"@web/core/debug/debug_context\";\n\nimport { useEffect } from \"@odoo/owl\";\n\nexport class ActionDialog extends Dialog {\n    static components = { ...Dialog.components, DebugMenu };\n    static template = \"web.ActionDialog\";\n    static props = {\n        ...Dialog.props,\n        close: Function,\n        slots: { optional: true },\n        ActionComponent: { optional: true },\n        actionProps: { optional: true },\n        actionType: { optional: true },\n        title: { optional: true },\n    };\n    static defaultProps = {\n        ...Dialog.defaultProps,\n        withBodyPadding: false,\n    };\n\n    setup() {\n        super.setup();\n        useOwnDebugContext();\n        useEffect(\n            () => {\n                if (this.modalRef.el.querySelector(\".modal-footer\")?.childElementCount > 1) {\n                    const defaultButton = this.modalRef.el.querySelector(\n                        \".modal-footer button.o-default-button\"\n                    );\n                    defaultButton.classList.add(\"d-none\");\n                }\n            },\n            () => []\n        );\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { standardActionServiceProps } from \"./action_service\";\n\nimport { Component, onWillStart } from \"@odoo/owl\";\n\n/**\n * Client action to use in a dialog to display the URL of a Kiosk, containing a\n * link to Install the corresponding PWA\n */\nexport class InstallKiosk extends Component {\n    static template = \"web.ActionInstallKioskPWA\";\n    static props = { ...standardActionServiceProps };\n\n    setup() {\n        this.resModel = this.props.action.res_model;\n        this.orm = useService(\"orm\");\n        this.dialog = useService(\"dialog\");\n        onWillStart(async () => {\n            this.url = await this.orm.call(this.resModel, \"get_kiosk_url\", [\n                this.props.action.context.active_id,\n            ]);\n        });\n    }\n\n    get appId() {\n        return this.props.action.context.app_id || this.resModel;\n    }\n\n    get installURL() {\n        return `/scoped_app?app_id=${this.appId}&path=${encodeURIComponent(\n            this.url.replace(document.location.origin + \"/\", \"\")\n        )}`;\n    }\n}\n\nregistry.category(\"actions\").add(\"install_kiosk_pwa\", InstallKiosk);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { makeContext } from \"@web/core/context\";\nimport { useDebugCategory } from \"@web/core/debug/debug_context\";\nimport { evaluateExpr } from \"@web/core/py_js/py\";\nimport { rpc, rpcBus } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { Deferred, KeepLast } from \"@web/core/utils/concurrency\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { View, ViewNotFoundError } from \"@web/views/view\";\nimport { ActionDialog } from \"./action_dialog\";\nimport { ReportAction } from \"./reports/report_action\";\nimport { UPDATE_METHODS } from \"@web/core/orm_service\";\nimport { CallbackRecorder } from \"@web/search/action_hook\";\nimport { ControlPanel } from \"@web/search/control_panel/control_panel\";\nimport { PATH_KEYS, router as _router, stateToUrl } from \"@web/core/browser/router\";\n\nimport {\n    Component,\n    markup,\n    onMounted,\n    onWillUnmount,\n    onError,\n    useChildSubEnv,\n    xml,\n    reactive,\n    status,\n} from \"@odoo/owl\";\nimport { downloadReport, getReportUrl } from \"./reports/utils\";\nimport { zip } from \"@web/core/utils/arrays\";\nimport { isHtmlEmpty } from \"@web/core/utils/html\";\nimport { omit, pick, shallowEqual } from \"@web/core/utils/objects\";\nimport { session } from \"@web/session\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\n\nclass BlankComponent extends Component {\n    static props = [\"onMounted\", \"withControlPanel\", \"*\"];\n    static template = xml`\n        <ControlPanel display=\"{disableDropdown: true}\" t-if=\"props.withControlPanel and !env.isSmall\">\n            <t t-set-slot=\"layout-buttons\">\n                <button class=\"btn btn-primary invisible\"> empty </button>\n            </t>\n        </ControlPanel>`;\n    static components = { ControlPanel };\n\n    setup() {\n        useChildSubEnv({ config: { breadcrumbs: [], noBreadcrumbs: true } });\n        onMounted(() => this.props.onMounted());\n    }\n}\n\nconst actionHandlersRegistry = registry.category(\"action_handlers\");\nconst actionRegistry = registry.category(\"actions\");\n\n/** @typedef {number|false} ActionId */\n/** @typedef {Object} ActionDescription */\n/** @typedef {\"current\" | \"fullscreen\" | \"new\" | \"main\" | \"self\" | \"inline\"} ActionMode */\n/** @typedef {string} ActionTag */\n/** @typedef {string} ActionXMLId */\n/** @typedef {Object} Context */\n/** @typedef {Function} CallableFunction */\n/** @typedef {string} ViewType */\n\n/** @typedef {ActionId|ActionXMLId|ActionTag|ActionDescription} ActionRequest */\n\n/**\n * @typedef {Object} ActionOptions\n * @property {Context} [additionalContext]\n * @property {boolean} [clearBreadcrumbs]\n * @property {CallableFunction} [onClose]\n * @property {Object} [props]\n * @property {ViewType} [viewType]\n * @property {\"replaceCurrentAction\" | \"replacePreviousAction\"} [stackPosition]\n * @property {number} [index]\n */\n\nexport async function clearUncommittedChanges(env) {\n    const callbacks = [];\n    env.bus.trigger(\"CLEAR-UNCOMMITTED-CHANGES\", callbacks);\n    const res = await Promise.all(callbacks.map((fn) => fn()));\n    return !res.includes(false);\n}\n\nexport const standardActionServiceProps = {\n    action: Object, // prop added by _getActionInfo\n    actionId: { type: Number, optional: true }, // prop added by _getActionInfo\n    className: { type: String, optional: true }, // prop added by the ActionContainer\n    globalState: { type: Object, optional: true }, // prop added by _updateUI\n    state: { type: Object, optional: true }, // prop added by _updateUI\n    resId: { type: [Number, Boolean], optional: true },\n    updateActionState: { type: Function, optional: true },\n};\n\nfunction parseActiveIds(ids) {\n    const activeIds = [];\n    if (typeof ids === \"string\") {\n        activeIds.push(...ids.split(\",\").map(Number));\n    } else if (typeof ids === \"number\") {\n        activeIds.push(ids);\n    }\n    return activeIds;\n}\n\nconst DIALOG_SIZES = {\n    \"extra-large\": \"xl\",\n    large: \"lg\",\n    medium: \"md\",\n    small: \"sm\",\n};\n\n// -----------------------------------------------------------------------------\n// Errors\n// -----------------------------------------------------------------------------\n\nexport class ControllerNotFoundError extends Error {}\n\nexport class InvalidButtonParamsError extends Error {}\n\n// -----------------------------------------------------------------------------\n// ActionManager (Service)\n// -----------------------------------------------------------------------------\n\n// regex that matches context keys not to forward from an action to another\nconst CTX_KEY_REGEX =\n    /^(?:(?:default_|search_default_|show_).+|.+_view_ref|group_by|active_id|active_ids|orderedBy)$/;\n// keys added to the context for the embedded actions feature\nconst EMBEDDED_ACTIONS_CTX_KEYS = [\n    \"current_embedded_action_id\",\n    \"parent_action_embedded_actions\",\n    \"parent_action_id\",\n    \"from_embedded_action\",\n];\n\n// only register this template once for all dynamic classes ControllerComponent\nconst ControllerComponentTemplate = xml`<t t-component=\"Component\" t-props=\"componentProps\"/>`;\n\nexport function makeActionManager(env, router = _router) {\n    const breadcrumbCache = {};\n    const keepLast = new KeepLast();\n    let id = 0;\n    let controllerStack = [];\n    let dialogCloseProm;\n    let actionCache = {};\n    let dialog = null;\n    let nextDialog = null;\n\n    router.hideKeyFromUrl(\"globalState\");\n\n    env.bus.addEventListener(\"CLEAR-CACHES\", () => {\n        actionCache = {};\n    });\n    rpcBus.addEventListener(\"RPC:RESPONSE\", (ev) => {\n        const { model, method } = ev.detail.data.params;\n        if (model === \"ir.actions.act_window\" && UPDATE_METHODS.includes(method)) {\n            actionCache = {};\n        }\n    });\n\n    // ---------------------------------------------------------------------------\n    // misc\n    // ---------------------------------------------------------------------------\n\n    /**\n     * Create an array of virtual controllers based on the current state of the\n     * router.\n     *\n     * @returns {Promise<object[]>} an array of virtual controllers\n     */\n    async function _controllersFromState() {\n        const state = router.current;\n        if (!state?.actionStack?.length) {\n            return [];\n        }\n        // The last controller will be created by doAction and won't be virtual\n        const controllers = state.actionStack\n            .slice(0, -1)\n            .map((actionState, index) => {\n                const controller = _makeController({\n                    displayName: actionState.displayName,\n                    virtual: true,\n                    action: {},\n                    props: {},\n                    state: { ...actionState, actionStack: state.actionStack.slice(0, index + 1) },\n                    currentState: {},\n                });\n                if (actionState.action) {\n                    controller.action.id = actionState.action;\n\n                    const [actionRequestKey, clientAction] = actionRegistry.contains(\n                        actionState.action\n                    )\n                        ? [actionState.action, actionRegistry.get(actionState.action)]\n                        : actionRegistry\n                              .getEntries()\n                              .find((a) => a[1].path === actionState.action) ?? [];\n                    if (actionRequestKey && clientAction) {\n                        if (state.actionStack[index + 1]?.action === actionState.action) {\n                            // client actions don't have multi-record views, so we can't go further to the next controller\n                            return;\n                        }\n                        controller.action.tag = actionRequestKey;\n                        controller.action.type = \"ir.actions.client\";\n                        controller.displayName = clientAction.displayName?.toString();\n                    }\n                    if (actionState.active_id) {\n                        controller.action.context = { active_id: actionState.active_id };\n                        controller.currentState.active_id = actionState.active_id;\n                    }\n                }\n                if (actionState.model) {\n                    controller.action.type = \"ir.actions.act_window\";\n                    controller.props.resModel = actionState.model;\n                }\n                if (actionState.resId) {\n                    controller.action.type ||= \"ir.actions.act_window\";\n                    controller.props.resId = actionState.resId;\n                    controller.currentState.resId = actionState.resId;\n                    controller.props.type = \"form\";\n                }\n                return controller;\n            })\n            .filter(Boolean);\n\n        if (state.action && state.resId && controllers.at(-1)?.action?.id === state.action) {\n            // When loading the state on a form view, we will need to load the action for it,\n            // and this will give us the display name of the corresponding multi-record view in\n            // the breadcrumb.\n            // By marking the last controller as a lazyController, we can in some cases avoid\n            // _loadBreadcrumbs from doing any network request as the breadcrumbs may only contain\n            // the form view and the multi-record view.\n            const bcControllers = await _loadBreadcrumbs(controllers.slice(0, -1));\n            controllers.at(-1).lazy = true;\n            return [...bcControllers, controllers.at(-1)];\n        }\n        return _loadBreadcrumbs(controllers);\n    }\n\n    /**\n     * Load breadcrumbs for an array of controllers. This function adds display\n     * names to controllers that the current user has access to and for which\n     * the view (and record) exist. Controllers that correspond to a deleted\n     * record or a record/view that the user can't access are removed.\n     *\n     * @param {object[]} controllers an array of controllers whose breadcrumbs\n     *  should be loaded\n     * @returns {Promise<object[]>} a new array of the displayable controllers\n     *  to which a display name was added\n     */\n    async function _loadBreadcrumbs(controllers) {\n        const toFetch = [];\n        const keys = [];\n        for (const { action, state, displayName } of controllers) {\n            if (action.id === \"menu\" || (action.type === \"ir.actions.client\" && !displayName)) {\n                continue;\n            }\n            const actionInfo = pick(state, \"action\", \"model\", \"resId\");\n            const key = JSON.stringify(actionInfo);\n            keys.push(key);\n            if (displayName) {\n                breadcrumbCache[key] = { display_name: displayName };\n            }\n            if (key in breadcrumbCache) {\n                continue;\n            }\n            toFetch.push(actionInfo);\n        }\n        if (toFetch.length) {\n            const req = rpc(\"/web/action/load_breadcrumbs\", { actions: toFetch });\n            for (const [i, info] of toFetch.entries()) {\n                const key = JSON.stringify(info);\n                breadcrumbCache[key] = req.then((res) => {\n                    breadcrumbCache[key] = res[i];\n                    return res[i];\n                });\n            }\n        }\n        const results = await Promise.all(keys.map((k) => breadcrumbCache[k]));\n        const controllersToRemove = [];\n        for (const [controller, res] of zip(controllers, results)) {\n            if (\"display_name\" in res) {\n                controller.displayName = res.display_name;\n            } else {\n                controllersToRemove.push(controller);\n                if (\"error\" in res) {\n                    console.warn(\n                        \"The following element was removed from the breadcrumb and from the url.\\n\",\n                        controller.state,\n                        \"\\nThis could be because the action wasn't found or because the user doesn't have the right to access to the record, the original error is :\\n\",\n                        res.error\n                    );\n                }\n            }\n        }\n        return controllers.filter((c) => !controllersToRemove.includes(c));\n    }\n\n    /**\n     * Removes the current dialog from the action service's state.\n     * It returns the dialog's onClose callback to be able to propagate it to the next dialog.\n     *\n     * @return {Function|undefined} When there was a dialog, returns its onClose callback for propagation to next dialog.\n     */\n    function _removeDialog() {\n        if (dialog) {\n            const { onClose, remove } = dialog;\n            dialog = null;\n            // Remove the dialog from the dialog_service.\n            // The code is well enough designed to avoid falling in a function call loop.\n            remove();\n            return onClose;\n        }\n    }\n\n    /**\n     * Returns the last controller of the current controller stack.\n     *\n     * @returns {Controller|null}\n     */\n    function _getCurrentController() {\n        const stack = controllerStack;\n        return stack.length ? stack[stack.length - 1] : null;\n    }\n\n    /**\n     * Given an id, xmlid, tag (key of the client action registry) or directly an\n     * object describing an action.\n     *\n     * @private\n     * @param {ActionRequest} actionRequest\n     * @param {Context} [context={}]\n     * @returns {Promise<Action>}\n     */\n    async function _loadAction(actionRequest, context = {}) {\n        if (typeof actionRequest === \"string\" && actionRegistry.contains(actionRequest)) {\n            // actionRequest is a key in the actionRegistry\n            return {\n                target: \"current\",\n                tag: actionRequest,\n                type: \"ir.actions.client\",\n            };\n        }\n\n        if (typeof actionRequest === \"string\" || typeof actionRequest === \"number\") {\n            // actionRequest is an id or an xmlid\n            const ctx = makeContext([user.context, context]);\n            delete ctx.params;\n            const key = `${JSON.stringify(actionRequest)},${JSON.stringify(ctx)}`;\n            let action = await actionCache[key];\n            if (!action) {\n                actionCache[key] = rpc(\"/web/action/load\", {\n                    action_id: actionRequest,\n                    context: ctx,\n                });\n                action = await actionCache[key];\n                if (action.help) {\n                    action.help = markup(action.help);\n                }\n            }\n            return Object.assign({}, action);\n        }\n\n        // actionRequest is an object describing the action\n        return actionRequest;\n    }\n\n    /**\n     * Makes a controller from the given params.\n     *\n     * @param {Object} params\n     * @returns {Controller}\n     */\n    function _makeController(params) {\n        return {\n            ...params,\n            jsId: `controller_${++id}`,\n            isMounted: false,\n        };\n    }\n\n    /**\n     * this function returns an action description\n     * with a unique jsId.\n     */\n    function _preprocessAction(action, context = {}) {\n        try {\n            action._originalAction = JSON.stringify(action);\n        } catch {\n            // do nothing, the action might simply not be serializable\n        }\n        action.context = makeContext([context, action.context], user.context);\n        const domain = action.domain || [];\n        action.domain =\n            typeof domain === \"string\"\n                ? evaluateExpr(domain, Object.assign({}, user.context, action.context))\n                : domain;\n        if (action.help) {\n            if (isHtmlEmpty(action.help)) {\n                delete action.help;\n            }\n        }\n        action = { ...action }; // manipulate a copy to keep cached action unmodified\n        action.jsId = `action_${++id}`;\n        if (action.type === \"ir.actions.act_window\" || action.type === \"ir.actions.client\") {\n            action.target = action.target || \"current\";\n        }\n        if (action.type === \"ir.actions.act_window\") {\n            action.views = [...action.views.map((v) => [v[0], v[1]])]; // manipulate a copy to keep cached action unmodified\n            action.controllers = {};\n            const target = action.target;\n            if (target !== \"inline\" && !(target === \"new\" && action.views[0][1] === \"form\")) {\n                // FIXME: search view arch is already sent with load_action, so either remove it\n                // from there or load all fieldviews alongside the action for the sake of consistency\n                const searchViewId = action.search_view_id ? action.search_view_id[0] : false;\n                action.views.push([searchViewId, \"search\"]);\n            }\n        }\n        return action;\n    }\n\n    /**\n     * @private\n     * @param {string} viewType\n     * @throws {Error} if the current controller is not a view\n     * @returns {View | null}\n     */\n    function _getView(viewType) {\n        const currentController = controllerStack[controllerStack.length - 1];\n        if (currentController.action.type !== \"ir.actions.act_window\") {\n            throw new Error(`switchView called but the current controller isn't a view`);\n        }\n        const view = currentController.views.find((view) => view.type === viewType);\n        return view || null;\n    }\n\n    /**\n     * Given a controller stack, returns the list of breadcrumb items.\n     *\n     * @private\n     * @param {ControllerStack} stack\n     * @returns {Breadcrumbs}\n     */\n    function _getBreadcrumbs(stack) {\n        return stack\n            .filter((controller) => controller.action.tag !== \"menu\")\n            .map((controller) => {\n                return {\n                    jsId: controller.jsId,\n                    get name() {\n                        return controller.displayName;\n                    },\n                    get isFormView() {\n                        return controller.props?.type === \"form\";\n                    },\n                    get url() {\n                        return stateToUrl(controller.state);\n                    },\n                    onSelected() {\n                        restore(controller.jsId);\n                    },\n                };\n            });\n    }\n\n    /**\n     * @private\n     * @param {object} [state] the state from which to get the action params\n     * @returns {{ actionRequest: object, options: object} | null}\n     */\n    function _getActionParams(state = router.current) {\n        const options = {};\n        let actionRequest = null;\n        if (state.action) {\n            const context = {};\n            if (state.active_id) {\n                context.active_id = state.active_id;\n            }\n            if (state.active_ids) {\n                context.active_ids = parseActiveIds(state.active_ids);\n            } else if (state.active_id) {\n                context.active_ids = [state.active_id];\n            }\n            // ClientAction\n            const [actionRequestKey, clientAction] = actionRegistry.contains(state.action)\n                ? [state.action, actionRegistry.get(state.action)]\n                : actionRegistry.getEntries().find((a) => a[1].path === state.action) ?? [];\n            if (actionRequestKey && clientAction) {\n                actionRequest = {\n                    context,\n                    params: state,\n                    tag: actionRequestKey,\n                    type: \"ir.actions.client\",\n                };\n                if (clientAction.path) {\n                    actionRequest.path = clientAction.path;\n                }\n            } else {\n                // The action to load isn't the current one => executes it\n                actionRequest = state.action;\n                context.params = state;\n                Object.assign(options, {\n                    additionalContext: context,\n                    viewType: state.resId ? \"form\" : state.view_type,\n                });\n            }\n            if ((state.resId && state.resId !== \"new\") || state.globalState) {\n                options.props = {};\n                if (state.resId && state.resId !== \"new\") {\n                    options.props.resId = state.resId;\n                }\n                if (state.globalState) {\n                    options.props.globalState = state.globalState;\n                }\n            }\n        } else if (state.model) {\n            if (state.resId || state.view_type === \"form\") {\n                actionRequest = {\n                    res_model: state.model,\n                    res_id: state.resId === \"new\" ? undefined : state.resId,\n                    type: \"ir.actions.act_window\",\n                    views: [[state.view_id ? state.view_id : false, \"form\"]],\n                };\n            } else {\n                // This is a window action on a multi-record view => restores it from\n                // the session storage\n                const storedAction = browser.sessionStorage.getItem(\"current_action\");\n                const lastAction = JSON.parse(storedAction || \"{}\");\n                if (lastAction.help) {\n                    lastAction.help = markup(lastAction.help);\n                }\n                if (lastAction.res_model === state.model) {\n                    if (lastAction.context) {\n                        // If this method is called because of a company switch, the\n                        // stored allowed_company_ids is incorrect.\n                        delete lastAction.context.allowed_company_ids;\n                    }\n                    actionRequest = lastAction;\n                    options.viewType = state.view_type;\n                }\n            }\n        }\n        if (!actionRequest) {\n            // If the last action isn't valid (eg a model with no resId and no view_type) which can\n            // happen if the user edits the url and removes the id from the end of the url, we don't want\n            // to send him back to the home menu: we unwind the actionStack until we find a valid action\n            const { actionStack } = state;\n            if (actionStack?.length > 1) {\n                const nextState = { actionStack: actionStack.slice(0, -1) };\n                Object.assign(nextState, nextState.actionStack.at(-1));\n                const params = _getActionParams(nextState);\n                // Place the controller at the found position in the action stack to remove all the\n                // invalid virtual controllers.\n                if (params.options && params.options.index === undefined) {\n                    params.options.index = nextState.actionStack.length - 1;\n                }\n                return params;\n            }\n            // Fall back to the home action if no valid action was found\n            actionRequest = user.homeActionId;\n        }\n        return actionRequest ? { actionRequest, options } : null;\n    }\n\n    /**\n     * @param {ClientAction} action\n     * @param {Object} props\n     * @returns {{ props: ActionProps, config: Config }}\n     */\n    function _getActionInfo(action, props) {\n        const actionProps = Object.assign({}, props, { action, actionId: action.id });\n        const currentState = {\n            resId: actionProps.resId || false,\n            active_id: action.context.active_id || false,\n        };\n        actionProps.updateActionState = (controller, patchState) => {\n            const oldState = { ...currentState };\n            Object.assign(currentState, patchState);\n            const changed = !shallowEqual(currentState, oldState);\n            if (changed && action.target !== \"new\" && controller.isMounted) {\n                pushState();\n            }\n        };\n        return {\n            props: actionProps,\n            currentState,\n            config: {\n                actionId: action.id,\n                actionType: \"ir.actions.client\",\n                actionFlags: action.flags,\n            },\n            displayName: action.display_name || action.name || \"\",\n        };\n    }\n\n    /**\n     * @param {Action} action\n     * @returns {ActionMode}\n     */\n    function _getActionMode(action) {\n        if (action.target === \"new\") {\n            // No possible override for target=\"new\"\n            return \"new\";\n        }\n        if (action.type === \"ir.actions.client\") {\n            const clientAction = actionRegistry.get(action.tag);\n            if (clientAction.target) {\n                // Target is forced by the definition of the client action\n                return clientAction.target;\n            }\n        }\n        if (action.target === \"fullscreen\") {\n            return \"fullscreen\";\n        }\n        // Default: current\n        return \"current\";\n    }\n\n    /**\n     * @param {BaseView} view\n     * @param {ActWindowAction} action\n     * @param {BaseView[]} views\n     * @param {Object} props\n     */\n    function _getViewInfo(view, action, views, props = {}) {\n        const target = action.target;\n        const viewSwitcherEntries = views\n            .filter((v) => v.multiRecord === view.multiRecord)\n            .map((v) => {\n                const viewSwitcherEntry = {\n                    icon: v.icon,\n                    name: v.display_name,\n                    type: v.type,\n                    multiRecord: v.multiRecord,\n                };\n                if (view.type === v.type) {\n                    viewSwitcherEntry.active = true;\n                }\n                return viewSwitcherEntry;\n            });\n        const context = action.context || {};\n        let groupBy = context.group_by || [];\n        if (typeof groupBy === \"string\") {\n            groupBy = [groupBy];\n        }\n        const openFormView = (resId, { activeIds, mode, force } = {}) => {\n            if (target !== \"new\") {\n                if (_getView(\"form\")) {\n                    return switchView(\"form\", { mode, resId, resIds: activeIds });\n                } else if (force || !resId) {\n                    return doAction(\n                        {\n                            type: \"ir.actions.act_window\",\n                            res_model: action.res_model,\n                            views: [[false, \"form\"]],\n                        },\n                        { props: { mode, resId, resIds: activeIds } }\n                    );\n                }\n            }\n        };\n        const viewProps = Object.assign({}, props, {\n            context,\n            display: { mode: target === \"new\" ? \"inDialog\" : target },\n            domain: action.domain || [],\n            groupBy,\n            loadActionMenus: target !== \"new\" && target !== \"inline\",\n            loadIrFilters: action.views.some((v) => v[1] === \"search\"),\n            resModel: action.res_model,\n            type: view.type,\n            selectRecord: openFormView,\n            createRecord: () => openFormView(false),\n        });\n        if (view.type === \"form\") {\n            if (target === \"new\") {\n                viewProps.mode = \"edit\";\n                if (!viewProps.onSave) {\n                    viewProps.onSave = (record, params) => {\n                        if (params && params.closable) {\n                            doAction({ type: \"ir.actions.act_window_close\" });\n                        }\n                    };\n                }\n            }\n            if (action.flags && \"mode\" in action.flags) {\n                viewProps.mode = action.flags.mode;\n            }\n        }\n\n        if (target === \"inline\") {\n            viewProps.searchMenuTypes = [];\n        }\n\n        const specialKeys = [\"help\", \"useSampleModel\", \"limit\", \"count\"];\n        for (const key of specialKeys) {\n            if (key in action) {\n                if (key === \"help\") {\n                    viewProps.noContentHelp = action.help;\n                } else {\n                    viewProps[key] = action[key];\n                }\n            }\n        }\n\n        if (context.search_disable_custom_filters) {\n            viewProps.activateFavorite = false;\n        }\n\n        // view specific\n        if (!viewProps.resId) {\n            viewProps.resId = action.res_id || false;\n        }\n\n        const currentState = {\n            resId: viewProps.resId,\n            active_id: action.context.active_id || false,\n        };\n        viewProps.updateActionState = (controller, patchState) => {\n            const oldState = { ...currentState };\n            Object.assign(currentState, patchState);\n            const changed = !shallowEqual(currentState, oldState);\n            if (changed && target !== \"new\" && controller.isMounted) {\n                pushState();\n            }\n        };\n\n        viewProps.noBreadcrumbs =\n            \"no_breadcrumbs\" in action.context ? action.context.no_breadcrumbs : target === \"new\";\n        delete action.context.no_breadcrumbs;\n\n        const embeddedActions =\n            view.type === \"form\"\n                ? []\n                : context.parent_action_embedded_actions || action.embedded_action_ids;\n        const parentActionId = (view.type !== \"form\" && context.parent_action_id) || false;\n        const currentEmbeddedActionId = context.current_embedded_action_id || false;\n        return {\n            props: viewProps,\n            currentState,\n            config: {\n                actionId: action.id,\n                actionName: action.name,\n                actionType: \"ir.actions.act_window\",\n                embeddedActions,\n                parentActionId,\n                currentEmbeddedActionId,\n                actionFlags: action.flags,\n                views: action.views,\n                viewSwitcherEntries,\n            },\n            displayName: action.display_name || action.name || \"\",\n        };\n    }\n\n    /**\n     * Computes the position of the controller in the nextStack according to options\n     * @param {ActionOptions} options\n     */\n    function _computeStackIndex(options) {\n        if (options.clearBreadcrumbs) {\n            return 0;\n        } else if (options.stackPosition === \"replaceCurrentAction\") {\n            const currentController = controllerStack[controllerStack.length - 1];\n            if (currentController) {\n                return controllerStack.findIndex(\n                    (ct) => ct.action.jsId === currentController.action.jsId\n                );\n            }\n        } else if (options.stackPosition === \"replacePreviousAction\") {\n            let last;\n            for (let i = controllerStack.length - 1; i >= 0; i--) {\n                const action = controllerStack[i].action.jsId;\n                if (!last) {\n                    last = action;\n                }\n                if (action !== last) {\n                    last = action;\n                    break;\n                }\n            }\n            if (last) {\n                return controllerStack.findIndex((ct) => ct.action.jsId === last);\n            }\n            // TODO: throw if there is no previous action?\n        } else if (options.index !== undefined) {\n            return options.index;\n        }\n        return controllerStack.length;\n    }\n\n    /**\n     * Triggers a re-rendering with respect to the given controller.\n     *\n     * @private\n     * @param {Controller} controller\n     * @param {UpdateStackOptions} options\n     * @param {boolean} [options.clearBreadcrumbs=false]\n     * @param {number} [options.index]\n     * @returns {Promise<Number>}\n     */\n    async function _updateUI(controller, options = {}) {\n        let resolve;\n        let reject;\n        let dialogCloseResolve;\n        let removeDialogFn;\n        const currentActionProm = new Promise((_res, _rej) => {\n            resolve = _res;\n            reject = _rej;\n        });\n        const action = controller.action;\n        if (action.target !== \"new\" && \"newStack\" in options) {\n            controllerStack = options.newStack;\n        }\n        const index = _computeStackIndex(options);\n        const nextStack = [...controllerStack.slice(0, index), controller];\n        // Compute breadcrumbs\n        controller.config.breadcrumbs = reactive(\n            action.target === \"new\" ? [] : _getBreadcrumbs(nextStack)\n        );\n        controller.config.getDisplayName = () => controller.displayName;\n        controller.config.setDisplayName = (displayName) => {\n            controller.displayName = displayName;\n            if (controller === _getCurrentController()) {\n                // if not mounted yet, will be done in \"mounted\"\n                env.services.title.setParts({ action: controller.displayName });\n            }\n            if (action.target !== \"new\") {\n                // This is a hack to force the reactivity when a new displayName is set\n                controller.config.breadcrumbs.push(undefined);\n                controller.config.breadcrumbs.pop();\n            }\n        };\n        controller.config.setCurrentEmbeddedAction = (embeddedActionId) => {\n            controller.currentEmbeddedActionId = embeddedActionId;\n        };\n        controller.config.setEmbeddedActions = (embeddedActions) => {\n            controller.embeddedActions = embeddedActions;\n        };\n        controller.config.historyBack = () => {\n            if (dialog) {\n                _executeCloseAction();\n            } else {\n                const previousController = controllerStack[controllerStack.length - 2];\n                if (previousController) {\n                    restore(previousController.jsId);\n                } else {\n                    env.bus.trigger(\"WEBCLIENT:LOAD_DEFAULT_APP\");\n                }\n            }\n        };\n\n        class ControllerComponent extends Component {\n            static template = ControllerComponentTemplate;\n            static Component = controller.Component;\n            static props = {\n                \"*\": true,\n            };\n            setup() {\n                this.Component = controller.Component;\n                this.titleService = useService(\"title\");\n                useDebugCategory(\"action\", { action });\n                useChildSubEnv({\n                    config: controller.config,\n                    pushStateBeforeReload: () => {\n                        if (controller.isMounted) {\n                            return;\n                        }\n                        pushState(nextStack);\n                    },\n                });\n                if (action.target !== \"new\") {\n                    this.__beforeLeave__ = new CallbackRecorder();\n                    this.__getGlobalState__ = new CallbackRecorder();\n                    this.__getLocalState__ = new CallbackRecorder();\n                    useBus(env.bus, \"CLEAR-UNCOMMITTED-CHANGES\", (ev) => {\n                        const callbacks = ev.detail;\n                        const beforeLeaveFns = this.__beforeLeave__.callbacks;\n                        callbacks.push(...beforeLeaveFns);\n                    });\n                    if (this.constructor.Component !== View) {\n                        useChildSubEnv({\n                            __beforeLeave__: this.__beforeLeave__,\n                            __getGlobalState__: this.__getGlobalState__,\n                            __getLocalState__: this.__getLocalState__,\n                        });\n                    }\n                }\n\n                onMounted(this.onMounted);\n                onWillUnmount(this.onWillUnmount);\n                onError(this.onError);\n            }\n            onError(error) {\n                if (controller.isMounted) {\n                    // the error occurred on the controller which is\n                    // already in the DOM, so simply show the error\n                    Promise.reject(error);\n                    return;\n                }\n                if (!controller.isMounted && status(this) === \"mounted\") {\n                    // The error occured during an onMounted hook of one of the components.\n                    env.bus.trigger(\"ACTION_MANAGER:UPDATE\", {\n                        id: ++id,\n                        Component: BlankComponent,\n                        componentProps: {\n                            onMounted: () => {},\n                            withControlPanel: action.type === \"ir.actions.act_window\",\n                        },\n                    });\n                    Promise.reject(error);\n                    return;\n                }\n                // forward the error to the _updateUI caller then restore the action container\n                // to an unbroken state\n                reject(error);\n                if (action.target === \"new\") {\n                    removeDialogFn?.();\n                    return;\n                }\n                const index = controllerStack.findIndex((ct) => ct.jsId === controller.jsId);\n                if (index > 0) {\n                    // The error occurred while rendering an existing controller,\n                    // so go back to the previous controller, of the current faulty one.\n                    // This occurs when clicking on a breadcrumbs.\n                    return restore(controllerStack[index - 1].jsId);\n                }\n                if (index === 0) {\n                    // No previous controller to restore, so do nothing but display the error\n                    return;\n                }\n                const lastController = controllerStack.at(-1);\n                if (lastController) {\n                    if (lastController.jsId !== controller.jsId) {\n                        // the error occurred while rendering a new controller,\n                        // so go back to the last non faulty controller\n                        // (the error will be shown anyway as the promise\n                        // has been rejected)\n                        return restore(lastController.jsId);\n                    }\n                } else {\n                    env.bus.trigger(\"ACTION_MANAGER:UPDATE\", {});\n                }\n            }\n            onMounted() {\n                if (action.target === \"new\") {\n                    dialogCloseProm = new Promise((_r) => {\n                        dialogCloseResolve = _r;\n                    }).then(() => {\n                        dialogCloseProm = undefined;\n                    });\n                    dialog = nextDialog;\n                } else {\n                    controller.getGlobalState = () => {\n                        const exportFns = this.__getGlobalState__.callbacks;\n                        if (exportFns.length) {\n                            return Object.assign({}, ...exportFns.map((fn) => fn()));\n                        }\n                    };\n                    controller.getLocalState = () => {\n                        const exportFns = this.__getLocalState__.callbacks;\n                        if (exportFns.length) {\n                            return Object.assign({}, ...exportFns.map((fn) => fn()));\n                        }\n                    };\n\n                    controllerStack = nextStack; // the controller is mounted, commit the new stack\n                    pushState();\n                    this.titleService.setParts({ action: controller.displayName });\n                    browser.sessionStorage.setItem(\n                        \"current_action\",\n                        action._originalAction || \"{}\"\n                    );\n                }\n                resolve();\n                env.bus.trigger(\"ACTION_MANAGER:UI-UPDATED\", _getActionMode(action));\n                controller.isMounted = true;\n            }\n            onWillUnmount() {\n                controller.isMounted = false;\n                if (action.target === \"new\" && dialogCloseResolve) {\n                    dialogCloseResolve();\n                }\n            }\n            get componentProps() {\n                const componentProps = { ...this.props };\n                const updateActionState = componentProps.updateActionState;\n                componentProps.updateActionState = (newState) =>\n                    updateActionState(controller, newState);\n                if (this.constructor.Component === View) {\n                    componentProps.__beforeLeave__ = this.__beforeLeave__;\n                    componentProps.__getGlobalState__ = this.__getGlobalState__;\n                    componentProps.__getLocalState__ = this.__getLocalState__;\n                }\n                return componentProps;\n            }\n        }\n        if (action.target === \"new\") {\n            const actionDialogProps = {\n                ActionComponent: ControllerComponent,\n                actionProps: controller.props,\n                actionType: action.type,\n            };\n            if (action.name) {\n                actionDialogProps.title = action.name;\n            }\n            const size = DIALOG_SIZES[action.context.dialog_size];\n            if (size) {\n                actionDialogProps.size = size;\n            }\n            actionDialogProps.footer = action.context.footer ?? actionDialogProps.footer;\n            const onClose = _removeDialog();\n            removeDialogFn = env.services.dialog.add(ActionDialog, actionDialogProps, {\n                onClose: () => {\n                    const onClose = _removeDialog();\n                    if (onClose) {\n                        onClose();\n                    }\n                },\n            });\n            if (nextDialog) {\n                nextDialog.remove();\n            }\n            nextDialog = {\n                remove: removeDialogFn,\n                onClose: onClose || options.onClose,\n            };\n            return currentActionProm;\n        }\n\n        const currentController = _getCurrentController();\n        if (currentController && currentController.getLocalState) {\n            currentController.exportedState = currentController.getLocalState();\n        }\n        if (controller.exportedState) {\n            controller.props.state = controller.exportedState;\n        }\n\n        // TODO DAM Remarks:\n        // this thing seems useless for client actions.\n        // restore and switchView (at least) use this --> cannot be done in switchView only\n        // if prop globalState has been passed in doAction, since the action is new the prop won't be overridden in l655.\n        // if globalState is not useful for client actions --> maybe use that thing in useSetupView instead of useSetupAction?\n        // a good thing: the Object.assign seems to reflect the use of \"externalState\" in legacy Model class --> things should be fine.\n        if (currentController && currentController.getGlobalState) {\n            const globalState = Object.assign(\n                {},\n                currentController.action.globalState,\n                currentController.getGlobalState() // what if this = {}?\n            );\n\n            currentController.action.globalState = globalState;\n            // Avoid pushing the globalState, if the state on the router was changed.\n            // For instance, if a link was clicked, the state of the router will be the one of the link and not the one of the currentController.\n            // Or when using the back or forward buttons on the browser.\n            if (\n                currentController.state.action === router.current.action &&\n                currentController.state.active_id === router.current.active_id &&\n                currentController.state.resId === router.current.resId\n            ) {\n                router.pushState({ globalState }, { sync: true });\n            }\n        }\n        if (controller.action.globalState) {\n            controller.props.globalState = controller.action.globalState;\n        }\n\n        const closingProm = _executeCloseAction({ onCloseInfo: { noReload: true } });\n\n        if (options.clearBreadcrumbs && !options.noEmptyTransition) {\n            const def = new Deferred();\n            env.bus.trigger(\"ACTION_MANAGER:UPDATE\", {\n                id: ++id,\n                Component: BlankComponent,\n                componentProps: {\n                    onMounted: () => def.resolve(),\n                    withControlPanel: action.type === \"ir.actions.act_window\",\n                },\n            });\n            await def;\n        }\n        if (options.onActionReady) {\n            options.onActionReady(action);\n        }\n        controller.__info__ = {\n            id: ++id,\n            Component: ControllerComponent,\n            componentProps: controller.props,\n        };\n        env.services.dialog.closeAll();\n        env.bus.trigger(\"ACTION_MANAGER:UPDATE\", controller.__info__);\n        return Promise.all([currentActionProm, closingProm]).then((r) => r[0]);\n    }\n\n    // ---------------------------------------------------------------------------\n    // ir.actions.act_url\n    // ---------------------------------------------------------------------------\n\n    /**\n     * Executes actions of type 'ir.actions.act_url', i.e. redirects to the\n     * given url.\n     *\n     * @private\n     * @param {ActURLAction} action\n     * @param {ActionOptions} options\n     */\n    function _executeActURLAction(action, options) {\n        let url = action.url;\n        if (url && !(url.startsWith(\"http\") || url.startsWith(\"/\"))) {\n            url = \"/\" + url;\n        }\n        if (action.target === \"download\" || action.target === \"self\") {\n            browser.location.assign(url);\n        } else {\n            const w = browser.open(url, \"_blank\");\n            if (!w || w.closed || typeof w.closed === \"undefined\") {\n                const msg = _t(\n                    \"A popup window has been blocked. You may need to change your \" +\n                        \"browser settings to allow popup windows for this page.\"\n                );\n                env.services.notification.add(msg, {\n                    sticky: true,\n                    type: \"warning\",\n                });\n            }\n            if (action.close) {\n                return doAction(\n                    { type: \"ir.actions.act_window_close\" },\n                    { onClose: options.onClose }\n                );\n            } else if (options.onClose) {\n                options.onClose();\n            }\n        }\n    }\n\n    // ---------------------------------------------------------------------------\n    // ir.actions.act_window\n    // ---------------------------------------------------------------------------\n\n    /**\n     * Executes an action of type 'ir.actions.act_window'.\n     *\n     * @private\n     * @param {ActWindowAction} action\n     * @param {ActionOptions} options\n     */\n    async function _executeActWindowAction(action, options) {\n        const views = [];\n        const unknown = [];\n        for (const [, type] of action.views) {\n            if (type === \"search\") {\n                continue;\n            }\n            if (session.view_info[type]) {\n                const { icon, display_name, multi_record: multiRecord } = session.view_info[type];\n                views.push({ icon, display_name, multiRecord, type });\n            } else {\n                unknown.push(type);\n            }\n        }\n        if (unknown.length) {\n            throw new Error(\n                `View types not defined ${unknown.join(\", \")} found in act_window action ${\n                    action.id\n                }`\n            );\n        }\n        if (!views.length) {\n            throw new Error(`No view found for act_window action ${action.id}`);\n        }\n\n        let view = (options.viewType && views.find((v) => v.type === options.viewType)) || views[0];\n        if (env.isSmall) {\n            view = _findView(views, view.multiRecord, action.mobile_view_mode) || view;\n        }\n\n        const controller = _makeController({\n            Component: View,\n            action,\n            view,\n            views,\n            ..._getViewInfo(view, action, views, options.props),\n        });\n        action.controllers[view.type] = controller;\n\n        const newStackLastController = options.newStack?.at(-1);\n        if (newStackLastController?.lazy) {\n            const multiView = action.views.find(\n                (view) => view[1] !== \"form\" && view[1] !== \"search\"\n            );\n            if (multiView) {\n                // If the current action has a multi-record view, we add the last\n                // controller to the breadcrumb controllers.\n                delete newStackLastController.lazy;\n                newStackLastController.displayName = action.display_name || action.name || \"\";\n                newStackLastController.action = action;\n                newStackLastController.props.type = multiView[1];\n            } else {\n                // If the current action doesn't have a multi-record view,\n                // we don't need to add the last controller to the breadcrumb controllers\n                options.newStack.splice(-1);\n            }\n        }\n\n        return _updateUI(controller, options);\n    }\n\n    /**\n     * @private\n     * @param {Array} views an array of views\n     * @param {boolean} multiRecord true if we search for a multiRecord view\n     * @param {string} viewType type of the view to search\n     * @returns {Object|undefined} the requested view if it could be found\n     */\n    function _findView(views, multiRecord, viewType) {\n        return views.find((v) => v.type === viewType && v.multiRecord == multiRecord);\n    }\n\n    // ---------------------------------------------------------------------------\n    // ir.actions.client\n    // ---------------------------------------------------------------------------\n\n    /**\n     * Executes an action of type 'ir.actions.client'.\n     *\n     * @private\n     * @param {ClientAction} action\n     * @param {ActionOptions} options\n     */\n    async function _executeClientAction(action, options) {\n        const clientAction = actionRegistry.get(action.tag);\n        action.path ||= clientAction.path;\n        if (clientAction.prototype instanceof Component) {\n            if (action.target !== \"new\") {\n                const canProceed = await clearUncommittedChanges(env);\n                if (!canProceed) {\n                    return;\n                }\n                if (clientAction.target) {\n                    action.target = clientAction.target;\n                }\n            }\n            const controller = _makeController({\n                Component: clientAction,\n                action,\n                ..._getActionInfo(action, options.props),\n            });\n            controller.displayName ||= clientAction.displayName?.toString() || \"\";\n            return _updateUI(controller, options);\n        } else {\n            const next = await clientAction(env, action);\n            if (next) {\n                return doAction(next, options);\n            }\n        }\n    }\n\n    // ---------------------------------------------------------------------------\n    // ir.actions.report\n    // ---------------------------------------------------------------------------\n\n    function _executeReportClientAction(action, options) {\n        const props = Object.assign({}, options.props, {\n            data: action.data,\n            display_name: action.display_name,\n            name: action.name,\n            report_file: action.report_file,\n            report_name: action.report_name,\n            report_url: getReportUrl(action, \"html\", user.context),\n            context: Object.assign({}, action.context),\n        });\n\n        const controller = _makeController({\n            Component: ReportAction,\n            action,\n            ..._getActionInfo(action, props),\n        });\n\n        return _updateUI(controller, options);\n    }\n\n    /**\n     * Executes actions of type 'ir.actions.report'.\n     *\n     * @private\n     * @param {ReportAction} action\n     * @param {ActionOptions} options\n     */\n    async function _executeReportAction(action, options) {\n        const handlers = registry.category(\"ir.actions.report handlers\").getAll();\n        for (const handler of handlers) {\n            const result = await handler(action, options, env);\n            if (result) {\n                return result;\n            }\n        }\n        if (action.report_type === \"qweb-html\") {\n            return _executeReportClientAction(action, options);\n        } else if (action.report_type === \"qweb-pdf\" || action.report_type === \"qweb-text\") {\n            const type = action.report_type.slice(5);\n            let success, message;\n            env.services.ui.block();\n            try {\n                const downloadContext = { ...user.context };\n                if (action.context) {\n                    Object.assign(downloadContext, action.context);\n                }\n                ({ success, message } = await downloadReport(rpc, action, type, downloadContext));\n            } finally {\n                env.services.ui.unblock();\n            }\n            if (message) {\n                env.services.notification.add(message, {\n                    sticky: true,\n                    title: _t(\"Report\"),\n                });\n            }\n            if (!success) {\n                return _executeReportClientAction(action, options);\n            }\n            const { onClose } = options;\n            if (action.close_on_report_download) {\n                return doAction({ type: \"ir.actions.act_window_close\" }, { onClose });\n            } else if (onClose) {\n                onClose();\n            }\n        } else {\n            console.error(\n                `The ActionManager can't handle reports of type ${action.report_type}`,\n                action\n            );\n        }\n    }\n\n    // ---------------------------------------------------------------------------\n    // ir.actions.server\n    // ---------------------------------------------------------------------------\n\n    /**\n     * Executes an action of type 'ir.actions.server'.\n     *\n     * @private\n     * @param {ServerAction} action\n     * @param {ActionOptions} options\n     * @returns {Promise<void>}\n     */\n    async function _executeServerAction(action, options) {\n        const runProm = rpc(\"/web/action/run\", {\n            action_id: action.id,\n            context: makeContext([user.context, action.context]),\n        });\n        let nextAction = await keepLast.add(runProm);\n        if (nextAction.help) {\n            nextAction.help = markup(nextAction.help);\n        }\n        nextAction = nextAction || { type: \"ir.actions.act_window_close\" };\n        if (typeof nextAction === \"object\") {\n            nextAction.path ||= action.path;\n        }\n        return doAction(nextAction, options);\n    }\n\n    async function _executeCloseAction(params = {}) {\n        let onClose;\n        if (dialog) {\n            onClose = _removeDialog();\n        } else {\n            onClose = params.onClose;\n        }\n        if (onClose) {\n            await onClose(params.onCloseInfo);\n        }\n\n        return dialogCloseProm;\n    }\n\n    // ---------------------------------------------------------------------------\n    // public API\n    // ---------------------------------------------------------------------------\n\n    /**\n     * Main entry point of a 'doAction' request. Loads the action and executes it.\n     *\n     * @param {ActionRequest} actionRequest\n     * @param {ActionOptions} options\n     * @returns {Promise<number | undefined | void>}\n     */\n    async function doAction(actionRequest, options = {}) {\n        const actionProm = _loadAction(actionRequest, options.additionalContext);\n        let action = await keepLast.add(actionProm);\n        action = _preprocessAction(action, options.additionalContext);\n        options.clearBreadcrumbs = action.target === \"main\" || options.clearBreadcrumbs;\n        switch (action.type) {\n            case \"ir.actions.act_url\":\n                return _executeActURLAction(action, options);\n            case \"ir.actions.act_window\":\n                if (action.target !== \"new\") {\n                    const canProceed = await clearUncommittedChanges(env);\n                    if (!canProceed) {\n                        return new Promise(() => {});\n                    }\n                }\n                return _executeActWindowAction(action, options);\n            case \"ir.actions.act_window_close\":\n                return _executeCloseAction({ onClose: options.onClose, onCloseInfo: action.infos });\n            case \"ir.actions.client\":\n                return _executeClientAction(action, options);\n            case \"ir.actions.server\":\n                return _executeServerAction(action, options);\n            case \"ir.actions.report\":\n                return _executeReportAction(action, options);\n            default: {\n                const handler = actionHandlersRegistry.get(action.type, null);\n                if (handler !== null) {\n                    return handler({ env, action, options });\n                }\n                throw new Error(\n                    `The ActionManager service can't handle actions of type ${action.type}`\n                );\n            }\n        }\n    }\n\n    /**\n     * Executes an action on top of the current one (typically, when a button in a\n     * view is clicked). The button may be of type 'object' (call a given method\n     * of a given model) or 'action' (execute a given action). Alternatively, the\n     * button may have the attribute 'special', and in this case an\n     * 'ir.actions.act_window_close' is executed.\n     *\n     * @param {DoActionButtonParams} params\n     * @params {Object} [options={}]\n     * @params {boolean} [options.isEmbeddedAction] set to true if the action request is an\n     *  embedded action. This allows to do the necessary context cleanup and avoid infinite\n     *  recursion.\n     * @returns {Promise<void>}\n     */\n    async function doActionButton(params, { isEmbeddedAction } = {}) {\n        // determine the action to execute according to the params\n        let action;\n        if (!isEmbeddedAction) {\n            for (const key of EMBEDDED_ACTIONS_CTX_KEYS) {\n                delete params.context?.[key];\n            }\n        }\n        const context = makeContext([params.context, params.buttonContext]);\n        const blockUi = exprToBoolean(params[\"block-ui\"]);\n        if (blockUi) {\n            env.services.ui.block();\n        }\n        if (params.special) {\n            action = { type: \"ir.actions.act_window_close\", infos: { special: true } };\n        } else if (params.type === \"object\") {\n            // call a Python Object method, which may return an action to execute\n            let args = params.resId ? [[params.resId]] : [params.resIds];\n            if (params.args) {\n                let additionalArgs;\n                try {\n                    // warning: quotes and double quotes problem due to json and xml clash\n                    // maybe we should force escaping in xml or do a better parse of the args array\n                    additionalArgs = JSON.parse(params.args.replace(/'/g, '\"'));\n                } catch {\n                    browser.console.error(\"Could not JSON.parse arguments\", params.args);\n                }\n                args = args.concat(additionalArgs);\n            }\n            const callProm = rpc(`/web/dataset/call_button/${params.resModel}/${params.name}`, {\n                args,\n                kwargs: { context },\n                method: params.name,\n                model: params.resModel,\n            });\n            action = await keepLast.add(callProm);\n            action =\n                action && typeof action === \"object\"\n                    ? action\n                    : { type: \"ir.actions.act_window_close\" };\n            if (action.help) {\n                action.help = markup(action.help);\n            }\n        } else if (params.type === \"action\") {\n            // execute a given action, so load it first\n            context.active_id = params.resId || null;\n            context.active_ids = params.resIds;\n            context.active_model = params.resModel;\n            action = await keepLast.add(_loadAction(params.name, context));\n        } else {\n            if (blockUi) {\n                env.services.ui.unblock();\n            }\n            throw new InvalidButtonParamsError(\"Missing type for doActionButton request\");\n        }\n        if (!isEmbeddedAction && action.embedded_action_ids?.length) {\n            const embeddedActionsOrder = JSON.parse(\n                browser.localStorage.getItem(\n                    `orderEmbedded${action.id}+${params.resId || \"\"}+${user.userId}`\n                )\n            );\n            const embeddedActionId = embeddedActionsOrder?.[0];\n            const embeddedAction = action.embedded_action_ids?.find(\n                (embeddedAction) => embeddedAction.id === embeddedActionId\n            );\n            if (embeddedAction) {\n                const embeddedActions = [\n                    ...action.embedded_action_ids,\n                    {\n                        id: false,\n                        name: action.name,\n                        parent_action_id: action.id,\n                        parent_res_model: action.res_model,\n                        action_id: action.id,\n                        user_id: false,\n                        context: {},\n                    },\n                ];\n                const context = {\n                    ...action.context,\n                    ...(embeddedAction.context ? makeContext([embeddedAction.context]) : {}),\n                    active_id: params.resId,\n                    active_model: params.resModel,\n                    current_embedded_action_id: embeddedActionId,\n                    parent_action_embedded_actions: embeddedActions,\n                    parent_action_id: action.id,\n                };\n                await this.doActionButton(\n                    {\n                        name:\n                            embeddedAction.python_method ||\n                            embeddedAction.action_id[0] ||\n                            embeddedAction.action_id,\n                        resId: params.resId,\n                        context,\n                        type: embeddedAction.python_method ? \"object\" : \"action\",\n                        resModel: embeddedAction.parent_res_model,\n                        viewType: embeddedAction.default_view_mode,\n                    },\n                    { isEmbeddedAction: true }\n                );\n                return;\n            }\n        }\n        // filter out context keys that are specific to the current action, because:\n        //  - wrong default_* and search_default_* values won't give the expected result\n        //  - wrong group_by values will fail and forbid rendering of the destination view\n        const currentCtx = {};\n        for (const key in params.context) {\n            if (key.match(CTX_KEY_REGEX) === null) {\n                currentCtx[key] = params.context[key];\n            }\n        }\n        const activeCtx = { active_model: params.resModel };\n        if (params.resId) {\n            activeCtx.active_id = params.resId;\n            activeCtx.active_ids = [params.resId];\n        }\n        action.context = makeContext([currentCtx, params.buttonContext, activeCtx, action.context]);\n        // in case an effect is returned from python and there is already an effect\n        // attribute on the button, the priority is given to the button attribute\n        const effect = params.effect ? evaluateExpr(params.effect) : action.effect;\n        const { onClose, stackPosition, viewType } = params;\n        const options = { onClose, stackPosition, viewType };\n        await doAction(action, options);\n        if (params.close) {\n            await _executeCloseAction();\n        }\n        if (blockUi) {\n            env.services.ui.unblock();\n        }\n        if (effect) {\n            env.services.effect.add(effect);\n        }\n    }\n\n    /**\n     * Switches to the given view type in action of the last controller of the\n     * stack. This action must be of type 'ir.actions.act_window'.\n     *\n     * @param {ViewType} viewType\n     * @param {Object} [props={}]\n     * @throws {ViewNotFoundError} if the viewType is not found on the current action\n     * @returns {Promise<Number>}\n     */\n    async function switchView(viewType, props = {}) {\n        await keepLast.add(Promise.resolve());\n        if (dialog) {\n            // we don't want to switch view when there's a dialog open, as we would\n            // not switch in the correct action (action in background != dialog action)\n            return;\n        }\n        const controller = controllerStack[controllerStack.length - 1];\n        const view = _getView(viewType);\n        if (!view) {\n            throw new ViewNotFoundError(\n                _t(\"No view of type '%s' could be found in the current action.\", viewType)\n            );\n        }\n        const newController =\n            controller.action.controllers[viewType] ||\n            _makeController({\n                Component: View,\n                action: controller.action,\n                views: controller.views,\n                view,\n            });\n\n        const canProceed = await clearUncommittedChanges(env);\n        if (!canProceed) {\n            return;\n        }\n\n        Object.assign(\n            newController,\n            _getViewInfo(view, controller.action, controller.views, props)\n        );\n        controller.action.controllers[viewType] = newController;\n        let index;\n        if (view.multiRecord) {\n            index = controllerStack.findIndex((ct) => ct.action.jsId === controller.action.jsId);\n            index = index > -1 ? index : controllerStack.length - 1;\n        } else {\n            // This case would mostly happen when loadState detects a change in the URL.\n            // Also, I guess we may need it when we have other monoRecord views\n            index = controllerStack.findIndex(\n                (ct) => ct.action.jsId === controller.action.jsId && !ct.view.multiRecord\n            );\n            index = index > -1 ? index : controllerStack.length;\n        }\n        return _updateUI(newController, { index });\n    }\n\n    /**\n     * Restores a controller from the controller stack given its id. Typically,\n     * this function is called when clicking on the breadcrumbs. If no id is given\n     * restores the previous controller from the stack (penultimate).\n     *\n     * @param {string} jsId\n     */\n    async function restore(jsId) {\n        await keepLast.add(Promise.resolve());\n        let index;\n        if (!jsId) {\n            index = controllerStack.length - 2;\n        } else {\n            index = controllerStack.findIndex((controller) => controller.jsId === jsId);\n        }\n        if (index < 0) {\n            const msg = jsId ? \"Invalid controller to restore\" : \"No controller to restore\";\n            throw new ControllerNotFoundError(msg);\n        }\n        const canProceed = await clearUncommittedChanges(env);\n        if (!canProceed) {\n            return;\n        }\n        const controller = controllerStack[index];\n        if (controller.virtual) {\n            const actionParams = _getActionParams(controller.state);\n            if (!actionParams) {\n                throw new Error(\"Attempted to restore a virtual controller whose state is invalid\");\n            }\n            const { actionRequest, options } = actionParams;\n            controllerStack = controllerStack.slice(0, index);\n            return doAction(actionRequest, options);\n        }\n        if (controller.action.type === \"ir.actions.act_window\") {\n            if (controller.isMounted) {\n                controller.exportedState = controller.getLocalState();\n            }\n            const { action, exportedState, view, views } = controller;\n            const props = { ...controller.props };\n            if (exportedState && \"resId\" in exportedState) {\n                // When restoring, we want to use the last exported ID of the controller\n                props.resId = exportedState.resId;\n            }\n            Object.assign(controller, _getViewInfo(view, action, views, props));\n        }\n        return _updateUI(controller, { index });\n    }\n\n    /**\n     * Restores a stack of virtual controllers from the current contents of the\n     * URL and performs a \"doAction\" on the last one.\n     *\n     * @returns {Promise<boolean>} true if doAction was performed\n     */\n    async function loadState() {\n        const newStack = await _controllersFromState();\n        const actionParams = _getActionParams();\n        if (actionParams) {\n            // Params valid => performs a \"doAction\"\n            const { actionRequest, options } = actionParams;\n            if (options.index) {\n                options.newStack = newStack.slice(0, options.index);\n                delete options.index;\n            } else {\n                options.newStack = newStack;\n            }\n            await doAction(actionRequest, options);\n            return true;\n        }\n    }\n\n    function pushState(cStack = controllerStack) {\n        if (!cStack.length) {\n            return;\n        }\n        const actions = cStack.map((controller) => {\n            const { action, props, displayName } = controller;\n            const actionState = { displayName };\n            if (action.path || action.id) {\n                actionState.action = action.path || action.id;\n            } else if (action.type === \"ir.actions.client\") {\n                actionState.action = action.tag;\n            } else if (action.type === \"ir.actions.act_window\") {\n                actionState.model = props.resModel;\n            }\n            if (action.type === \"ir.actions.act_window\") {\n                actionState.view_type = props.type;\n                if (props.type === \"form\" && action.res_model !== \"res.config.settings\") {\n                    actionState.resId = controller.currentState.resId || \"new\";\n                }\n            }\n            if (action.type === \"ir.actions.client\" && controller.currentState?.resId) {\n                actionState.resId = controller.currentState.resId;\n            }\n\n            if (controller.currentState?.active_id) {\n                const activeId = controller.currentState.active_id;\n                if (activeId) {\n                    actionState.active_id = activeId;\n                }\n            }\n            Object.assign(actionState, omit(controller.currentState || {}, ...PATH_KEYS));\n            return actionState;\n        });\n        const newState = {\n            actionStack: actions,\n        };\n        const stateKeys = [...PATH_KEYS];\n        const { action, props, currentState } = cStack.at(-1);\n        if (props.type !== \"form\" && props.type !== action.views?.[0][1]) {\n            // add view_type only when it's not already known implicitly\n            stateKeys.push(\"view_type\");\n        }\n        if (currentState) {\n            stateKeys.push(...Object.keys(omit(currentState, ...PATH_KEYS)));\n        }\n        Object.assign(newState, pick(newState.actionStack.at(-1), ...stateKeys));\n\n        cStack.at(-1).state = newState;\n        router.pushState(newState, { replace: true });\n    }\n    return {\n        doAction,\n        doActionButton,\n        switchView,\n        restore,\n        loadState,\n        async loadAction(actionRequest, context) {\n            const action = await _loadAction(actionRequest, context);\n            return _preprocessAction(action, context);\n        },\n        get currentController() {\n            return _getCurrentController();\n        },\n    };\n}\n\nexport const actionService = {\n    dependencies: [\"dialog\", \"effect\", \"localization\", \"notification\", \"title\", \"ui\"],\n    start(env) {\n        return makeActionManager(env);\n    },\n};\n\nregistry.category(\"services\").add(\"action\", actionService);\n", "import { browser } from \"@web/core/browser/browser\";\nimport { router } from \"@web/core/browser/router\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { escape, sprintf } from \"@web/core/utils/strings\";\n\nimport { markup } from \"@odoo/owl\";\n\nexport function displayNotificationAction(env, action) {\n    const params = action.params || {};\n    const options = {\n        className: params.className || \"\",\n        sticky: params.sticky || false,\n        title: params.title,\n        type: params.type || \"info\",\n    };\n    const links = (params.links || []).map((link) => {\n        return `<a href=\"${escape(link.url)}\" target=\"_blank\">${escape(link.label)}</a>`;\n    });\n    const message = markup(sprintf(escape(params.message), ...links));\n    env.services.notification.add(message, options);\n    return params.next;\n}\n\nregistry.category(\"actions\").add(\"display_notification\", displayNotificationAction);\n\n/**\n * Client action to reload the whole interface.\n * If action.params.menu_id, it opens the given menu entry.\n * If action.params.action_id, it opens the given action.\n */\nfunction reload(env, action) {\n    const { menu_id, action_id } = action.params || {};\n    let route = { ...router.current };\n\n    if (menu_id || action_id) {\n        route = {};\n        if (menu_id) {\n            route.menu_id = menu_id;\n        }\n        if (action_id) {\n            route.action = action_id;\n        }\n    }\n\n    env.bus.trigger(\"CLEAR-CACHES\");\n    router.pushState(route, { replace: true, reload: true });\n}\n\nregistry.category(\"actions\").add(\"reload\", reload);\n\n/**\n * Client action to go back home.\n */\nasync function home() {\n    await new Promise((resolve) => {\n        const waitForServer = (delay) => {\n            browser.setTimeout(async () => {\n                rpc(\"/web/webclient/version_info\", {})\n                    .then(resolve)\n                    .catch(() => waitForServer(250));\n            }, delay);\n        };\n        waitForServer(1000);\n    });\n    const url = \"/\" + (browser.location.search || \"\");\n    browser.location.assign(url);\n}\n\nregistry.category(\"actions\").add(\"home\", home);\n\n/**\n * Client action to refresh the session context (making sure\n * HTTP requests will have the right one) then reload the\n * whole interface.\n */\nasync function reloadContext(env, action) {\n    // side-effect of get_session_info is to refresh the session context\n    await rpc(\"/web/session/get_session_info\");\n    reload(env, action);\n}\n\nregistry.category(\"actions\").add(\"reload_context\", reloadContext);\n\n/**\n * Client action to restore the current controller\n * Serves as a trigger to reload the interface without a full browser reload\n */\nasync function softReload(env, action) {\n    const controller = env.services.action.currentController;\n    if (controller) {\n        await env.services.action.restore(controller.jsId);\n    }\n}\n\nregistry.category(\"actions\").add(\"soft_reload\", softReload);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { editModelDebug } from \"@web/core/debug/debug_utils\";\nimport { registry } from \"@web/core/registry\";\n\nconst debugRegistry = registry.category(\"debug\");\n\nfunction editAction({ action, env }) {\n    if (!action.id) {\n        return null;\n    }\n    const description = _t(\"Action\");\n    return {\n        type: \"item\",\n        description,\n        callback: () => {\n            editModelDebug(env, description, action.type, action.id);\n        },\n        sequence: 220,\n        section: \"ui\",\n    };\n}\n\nfunction viewFields({ action, env }) {\n    if (!action.res_model) {\n        return null;\n    }\n    const description = _t(\"Fields\");\n    return {\n        type: \"item\",\n        description,\n        callback: async () => {\n            const modelId = (\n                await env.services.orm.search(\"ir.model\", [[\"model\", \"=\", action.res_model]], {\n                    limit: 1,\n                })\n            )[0];\n            env.services.action.doAction({\n                res_model: \"ir.model.fields\",\n                name: description,\n                views: [\n                    [false, \"list\"],\n                    [false, \"form\"],\n                ],\n                domain: [[\"model_id\", \"=\", modelId]],\n                type: \"ir.actions.act_window\",\n                context: {\n                    default_model_id: modelId,\n                },\n            });\n        },\n        sequence: 250,\n        section: \"ui\",\n    };\n}\n\nfunction ViewModel({ action, env }) {\n    if (!action.res_model) {\n        return null;\n    }\n    const modelName = action.res_model;\n    return {\n        type: \"item\",\n        description: _t(\"Model: %s\", modelName),\n        callback: async () => {\n            const modelId = (\n                await env.services.orm.search(\"ir.model\", [[\"model\", \"=\", modelName]], {\n                    limit: 1,\n                })\n            )[0];\n            editModelDebug(env, modelName, \"ir.model\", modelId);\n        },\n        sequence: 210,\n        section: \"ui\",\n    };\n}\n\nfunction manageFilters({ action, env }) {\n    if (!action.res_model) {\n        return null;\n    }\n    const description = _t(\"Filters\");\n    return {\n        type: \"item\",\n        description,\n        callback: () => {\n            // manage_filters\n            env.services.action.doAction({\n                res_model: \"ir.filters\",\n                name: description,\n                views: [\n                    [false, \"list\"],\n                    [false, \"form\"],\n                ],\n                type: \"ir.actions.act_window\",\n                context: {\n                    search_default_my_filters: true,\n                    search_default_model_id: action.res_model,\n                },\n            });\n        },\n        sequence: 260,\n        section: \"ui\",\n    };\n}\n\nfunction viewAccessRights({ accessRights, action, env }) {\n    if (!action.res_model || !accessRights.canSeeModelAccess) {\n        return null;\n    }\n    const description = _t(\"Access Rights\");\n    return {\n        type: \"item\",\n        description,\n        callback: async () => {\n            const modelId = (\n                await env.services.orm.search(\"ir.model\", [[\"model\", \"=\", action.res_model]], {\n                    limit: 1,\n                })\n            )[0];\n            env.services.action.doAction({\n                res_model: \"ir.model.access\",\n                name: description,\n                views: [\n                    [false, \"list\"],\n                    [false, \"form\"],\n                ],\n                domain: [[\"model_id\", \"=\", modelId]],\n                type: \"ir.actions.act_window\",\n                context: {\n                    default_model_id: modelId,\n                },\n            });\n        },\n        sequence: 350,\n        section: \"security\",\n    };\n}\n\nfunction viewRecordRules({ accessRights, action, env }) {\n    if (!action.res_model || !accessRights.canSeeRecordRules) {\n        return null;\n    }\n    const description = _t(\"Model Record Rules\");\n    return {\n        type: \"item\",\n        description: _t(\"Record Rules\"),\n        callback: async () => {\n            const modelId = (\n                await env.services.orm.search(\"ir.model\", [[\"model\", \"=\", action.res_model]], {\n                    limit: 1,\n                })\n            )[0];\n            env.services.action.doAction({\n                res_model: \"ir.rule\",\n                name: description,\n                views: [\n                    [false, \"list\"],\n                    [false, \"form\"],\n                ],\n                domain: [[\"model_id\", \"=\", modelId]],\n                type: \"ir.actions.act_window\",\n                context: {\n                    default_model_id: modelId,\n                },\n            });\n        },\n        sequence: 360,\n        section: \"security\",\n    };\n}\n\ndebugRegistry\n    .category(\"action\")\n    .add(\"editAction\", editAction)\n    .add(\"viewFields\", viewFields)\n    .add(\"ViewModel\", ViewModel)\n    .add(\"manageFilters\", manageFilters)\n    .add(\"viewAccessRights\", viewAccessRights)\n    .add(\"viewRecordRules\", viewRecordRules);\n", "import { registry } from \"@web/core/registry\";\nimport { Transition } from \"@web/core/transition\";\nimport { user } from \"@web/core/user\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { BurgerUserMenu } from \"./burger_user_menu/burger_user_menu\";\nimport { MobileSwitchCompanyMenu } from \"./mobile_switch_company_menu/mobile_switch_company_menu\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\n/**\n * This file includes the widget Menu in mobile to render the BurgerMenu which\n * opens fullscreen and displays the user menu and the current app submenus.\n */\n\nconst SWIPE_ACTIVATION_THRESHOLD = 100;\n\nexport class BurgerMenu extends Component {\n    static template = \"web.BurgerMenu\";\n    static props = {};\n    static components = {\n        BurgerUserMenu,\n        MobileSwitchCompanyMenu,\n        Transition,\n    };\n\n    setup() {\n        this.company = useService(\"company\");\n        this.user = user;\n        this.state = useState({\n            isBurgerOpened: false,\n        });\n        this.swipeStartX = null;\n        useBus(this.env.bus, \"HOME-MENU:TOGGLED\", () => {\n            this._closeBurger();\n        });\n        useBus(this.env.bus, \"ACTION_MANAGER:UPDATE\", ({ detail: req }) => {\n            if (req.id) {\n                this._closeBurger();\n            }\n        });\n    }\n    _closeBurger() {\n        this.state.isBurgerOpened = false;\n    }\n    _openBurger() {\n        this.state.isBurgerOpened = true;\n    }\n    _onSwipeStart(ev) {\n        this.swipeStartX = ev.changedTouches[0].clientX;\n    }\n    _onSwipeEnd(ev) {\n        if (!this.swipeStartX) {\n            return;\n        }\n        const deltaX = ev.changedTouches[0].clientX - this.swipeStartX;\n        if (deltaX < SWIPE_ACTIVATION_THRESHOLD) {\n            return;\n        }\n        this._closeBurger();\n        this.swipeStartX = null;\n    }\n}\n\nconst systrayItem = {\n    Component: BurgerMenu,\n};\n\nregistry.category(\"systray\").add(\"burger_menu\", systrayItem, { sequence: 0 });\n", "import { UserMenu } from \"@web/webclient/user_menu/user_menu\";\n\nexport class BurgerUserMenu extends UserMenu {\n    static template = \"web.BurgerUserMenu\";\n    _onItemClicked(callback) {\n        callback();\n    }\n}\n", "import { SwitchCompanyMenu } from \"@web/webclient/switch_company_menu/switch_company_menu\";\n\nexport class MobileSwitchCompanyMenu extends SwitchCompanyMenu {\n    static template = \"web.MobileSwitchCompanyMenu\";\n\n    setup() {\n        super.setup();\n        this.state.isOpen = false;\n    }\n\n    get show() {\n        return !this.hasLotsOfCompanies || this.state.isOpen === true;\n    }\n\n    toggleCollapsible() {\n        if (this.hasLotsOfCompanies) {\n            this.state.isOpen = !this.state.isOpen;\n        }\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { loadBundle } from \"@web/core/assets\";\nimport { registry } from \"@web/core/registry\";\nimport { browser } from \"@web/core/browser/browser\";\n\nexport async function startClickEverywhere(xmlId, light, currentState) {\n    await loadBundle(\"web.assets_clickbot\");\n    window.clickEverywhere(xmlId, light, currentState);\n}\n\nexport function runClickTestItem({ env }) {\n    return {\n        type: \"item\",\n        description: _t(\"Run Click Everywhere\"),\n        callback: () => {\n            startClickEverywhere();\n        },\n        sequence: 460,\n        section: \"testing\",\n    };\n}\n\nconst currentState = JSON.parse(browser.localStorage.getItem(\"running.clickbot\"));\nif (currentState) {\n    startClickEverywhere(currentState.xmlId, currentState.light, currentState);\n}\n\nexport default {\n    startClickEverywhere,\n    runClickTestItem,\n};\n\nregistry.category(\"debug\").category(\"default\").add(\"runClickTestItem\", runClickTestItem);\n", "import { browser } from \"@web/core/browser/browser\";\nimport { rpcBus } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { session } from \"@web/session\";\nimport { UPDATE_METHODS } from \"@web/core/orm_service\";\nimport { cookie } from \"@web/core/browser/cookie\";\nimport { user } from \"@web/core/user\";\nimport { router } from \"@web/core/browser/router\";\n\nconst CIDS_SEPARATOR = \"-\";\n\nfunction parseCompanyIds(cids, separator = CIDS_SEPARATOR) {\n    if (typeof cids === \"string\") {\n        return cids.split(separator).map(Number);\n    } else if (typeof cids === \"number\") {\n        return [cids];\n    }\n    return [];\n}\n\nfunction computeActiveCompanyIds(cids) {\n    const { user_companies } = session;\n    let activeCompanyIds = cids || [];\n    const availableCompaniesFromSession = user_companies.allowed_companies;\n    const notAllowedCompanies = activeCompanyIds.filter(\n        (id) => !(id in availableCompaniesFromSession)\n    );\n\n    if (!activeCompanyIds.length || notAllowedCompanies.length) {\n        activeCompanyIds = [user_companies.current_company];\n    }\n    return activeCompanyIds;\n}\n\nfunction getCompanyIds() {\n    let cids;\n    // backward compatibility, in old urls cid was still used.\n    // deprecated as of saas-17.3\n    const state = router.current;\n    if (\"cids\" in state) {\n        // backward compatibility s.t. old urls (still using \",\" as separator) keep working\n        // deprecated as of 17.0\n        if (typeof state.cids === \"string\" && !state.cids.includes(CIDS_SEPARATOR)) {\n            cids = parseCompanyIds(state.cids, \",\");\n        } else {\n            cids = parseCompanyIds(state.cids);\n        }\n    } else if (cookie.get(\"cids\")) {\n        cids = parseCompanyIds(cookie.get(\"cids\"));\n    }\n    return cids || [];\n}\n\nexport const companyService = {\n    dependencies: [\"action\", \"orm\"],\n    start(env, { action, orm }) {\n        const allowedCompanies = session.user_companies.allowed_companies;\n        const disallowedAncestorCompanies = session.user_companies.disallowed_ancestor_companies;\n        const allowedCompaniesWithAncestors = {\n            ...allowedCompanies,\n            ...disallowedAncestorCompanies,\n        };\n        const activeCompanyIds = computeActiveCompanyIds(getCompanyIds());\n\n        // update browser data\n        cookie.set(\"cids\", activeCompanyIds.join(CIDS_SEPARATOR));\n        user.updateContext({ allowed_company_ids: activeCompanyIds });\n\n        // reload the page if changes are being done to `res.company`\n        rpcBus.addEventListener(\"RPC:RESPONSE\", (ev) => {\n            const { data, error } = ev.detail;\n            const { model, method } = data.params;\n            if (!error && model === \"res.company\" && UPDATE_METHODS.includes(method)) {\n                if (!browser.localStorage.getItem(\"running_tour\")) {\n                    action.doAction(\"reload_context\");\n                }\n            }\n        });\n\n        return {\n            allowedCompanies,\n            allowedCompaniesWithAncestors,\n            disallowedAncestorCompanies,\n\n            get activeCompanyIds() {\n                return activeCompanyIds.slice();\n            },\n\n            get currentCompany() {\n                return allowedCompanies[activeCompanyIds[0]];\n            },\n\n            getCompany(companyId) {\n                return allowedCompaniesWithAncestors[companyId];\n            },\n\n            /**\n             * @param {Array<>} companyIds - List of companies to log into\n             * @param {boolean} [includeChildCompanies=true] - If true, will also\n             * log into each child of each companyIds (default is true)\n             */\n            async setCompanies(companyIds, includeChildCompanies = true) {\n                const newCompanyIds = companyIds.length ? companyIds : [activeCompanyIds[0]];\n\n                function addCompanies(companyIds) {\n                    for (const companyId of companyIds) {\n                        if (!newCompanyIds.includes(companyId)) {\n                            newCompanyIds.push(companyId);\n                            addCompanies(allowedCompanies[companyId].child_ids);\n                        }\n                    }\n                }\n\n                if (includeChildCompanies) {\n                    addCompanies(\n                        companyIds.flatMap((companyId) => allowedCompanies[companyId].child_ids)\n                    );\n                }\n\n                cookie.set(\"cids\", newCompanyIds.join(CIDS_SEPARATOR));\n                user.updateContext({ allowed_company_ids: newCompanyIds });\n\n                const controller = action.currentController;\n                const state = {};\n                const options = { reload: true };\n                if (controller?.props.resId && controller?.props.resModel) {\n                    const hasReadRights = await user.checkAccessRight(\n                        controller.props.resModel,\n                        \"read\",\n                        controller.props.resId\n                    );\n\n                    if (!hasReadRights) {\n                        options.replace = true;\n                        state.actionStack = router.current.actionStack.slice(0, -1);\n                    }\n                }\n\n                router.pushState(state, options);\n            },\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"company\", companyService);\n", "import { rpc, rpcBus } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { currencies } from \"@web/core/currency\";\nimport { UPDATE_METHODS } from \"@web/core/orm_service\";\n\nexport const currencyService = {\n    start() {\n        /**\n         * Reload the currencies (initially given in session_info)\n         */\n        async function reloadCurrencies() {\n            const result = await rpc(\"/web/session/get_session_info\");\n            for (const k in currencies) {\n                delete currencies[k];\n            }\n            Object.assign(currencies, result?.currencies);\n        }\n        rpcBus.addEventListener(\"RPC:RESPONSE\", (ev) => {\n            const { data, error } = ev.detail;\n            const { model, method } = data.params;\n            if (!error && model === \"res.currency\" && UPDATE_METHODS.includes(method)) {\n                reloadCurrencies();\n            }\n        });\n    },\n};\n\nregistry.category(\"services\").add(\"currency\", currencyService);\n", "import { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { SelectCreateDialog } from \"@web/views/view_dialogs/select_create_dialog\";\n\nfunction runUnitTestsItem() {\n    const href = \"/web/tests?debug=assets\";\n    return {\n        type: \"item\",\n        description: _t(\"Run Unit Tests\"),\n        href,\n        callback: () => browser.open(href),\n        sequence: 450,\n        section: \"testing\",\n    };\n}\n\nexport function openViewItem({ env }) {\n    async function onSelected(records) {\n        const views = await env.services.orm.searchRead(\n            \"ir.ui.view\",\n            [[\"id\", \"=\", records[0]]],\n            [\"name\", \"model\", \"type\"],\n            { limit: 1 }\n        );\n        const view = views[0];\n        env.services.action.doAction({\n            type: \"ir.actions.act_window\",\n            name: view.name,\n            res_model: view.model,\n            views: [[view.id, view.type]],\n        });\n    }\n\n    return {\n        type: \"item\",\n        description: _t(\"Open View\"),\n        callback: () => {\n            env.services.dialog.add(SelectCreateDialog, {\n                resModel: \"ir.ui.view\",\n                title: _t(\"Select a view\"),\n                multiSelect: false,\n                domain: [\n                    [\"type\", \"!=\", \"qweb\"],\n                    [\"type\", \"!=\", \"search\"],\n                ],\n                onSelected,\n            });\n        },\n        sequence: 540,\n        section: \"tools\",\n    };\n}\n\nregistry\n    .category(\"debug\")\n    .category(\"default\")\n    .add(\"runUnitTestsItem\", runUnitTestsItem)\n    .add(\"openViewItem\", openViewItem);\n", "import { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\n\nimport { Component, EventBus } from \"@odoo/owl\";\n\nexport class ProfilingItem extends Component {\n    static components = { DropdownItem };\n    static template = \"web.DebugMenu.ProfilingItem\";\n    static props = {\n        bus: { type: EventBus },\n    };\n    setup() {\n        this.profiling = useService(\"profiling\");\n        useBus(this.props.bus, \"UPDATE\", this.render);\n    }\n\n    changeParam(param, ev) {\n        this.profiling.setParam(param, ev.target.value);\n    }\n    toggleParam(param) {\n        const value = this.profiling.state.params.execution_context_qweb;\n        this.profiling.setParam(param, !value);\n    }\n    openProfiles() {\n        if (this.env.services.action) {\n            // using doAction in the backend to preserve breadcrumbs and stuff\n            this.env.services.action.doAction(\"base.action_menu_ir_profile\");\n        } else {\n            // No action service means we are in the frontend.\n            window.location = \"/web/#action=base.action_menu_ir_profile\";\n        }\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { loadBundle } from \"@web/core/assets\";\nimport { renderToString } from \"@web/core/utils/render\";\nimport { useDebounced } from \"@web/core/utils/timing\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nimport { Component, useState, useRef, onWillStart, onMounted, onWillUnmount } from \"@odoo/owl\";\n\nclass MenuItem extends Component {\n    static template = \"web.ProfilingQwebView.menuitem\";\n    static props = {\n        view: Object,\n    };\n}\n\nfunction processValue(value) {\n    const data = JSON.parse(value);\n    for (const line of data[0].results.data) {\n        line.xpath = line.xpath.replace(/([^\\]])\\//g, \"$1[1]/\").replace(/([^\\]])$/g, \"$1[1]\");\n    }\n    return data;\n}\n\n/**\n * This widget is intended to be used on Text fields. It will provide Ace Editor\n * for display XML and Python profiling.\n */\nexport class ProfilingQwebView extends Component {\n    static template = \"web.ProfilingQwebView\";\n    static components = { MenuItem };\n    static props = { ...standardFieldProps };\n\n    setup() {\n        super.setup();\n\n        this.orm = useService(\"orm\");\n        this.ace = useRef(\"ace\");\n        this.selector = useRef(\"selector\");\n\n        this.value = processValue(this.props.record.data[this.props.name]);\n        this.state = useState({\n            viewID: this.profile.data.length ? this.profile.data[0].view_id : 0,\n            view: null,\n        });\n\n        this.renderProfilingInformation = useDebounced(this.renderProfilingInformation, 100);\n\n        onWillStart(async () => {\n            await loadBundle(\"web.ace_lib\");\n            await this._fetchViewData();\n            this.state.view = this.viewObjects.find((view) => view.id === this.state.viewID);\n        });\n        onMounted(() => {\n            this._startAce(this.ace.el);\n            this._renderView();\n        });\n        onWillUnmount(() => {\n            if (this.aceEditor) {\n                this.aceEditor.destroy();\n            }\n            this._unmoutInfo();\n        });\n    }\n\n    /**\n     * Return JSON values to render the view\n     *\n     * @returns {archs, data: {template, xpath, directive, time, duration, query }[]}\n     */\n    get profile() {\n        return this.value ? this.value[0].results : { archs: {}, data: [] };\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * Return association of view key, view name, query number and total delay\n     *\n     * @private\n     * @returns {Promise<viewObjects>}\n     */\n    async _fetchViewData() {\n        const viewIDs = Array.from(new Set(this.profile.data.map((line) => line.view_id)));\n        const viewObjects = await this.orm.call(\"ir.ui.view\", \"search_read\", [], {\n            fields: [\"id\", \"display_name\", \"key\"],\n            domain: [[\"id\", \"in\", viewIDs]],\n        });\n        for (const view of viewObjects) {\n            view.delay = 0;\n            view.query = 0;\n            const lines = this.profile.data.filter((l) => l.view_id === view.id);\n            const root = lines.find((l) => l.xpath === \"\");\n            if (root) {\n                view.delay += root.delay;\n                view.query += root.query;\n            } else {\n                view.delay = lines.map((l) => l.delay).reduce((a, b) => a + b);\n                view.query = lines.map((l) => l.query).reduce((a, b) => a + b);\n            }\n            view.delay = Math.ceil(view.delay * 10) / 10;\n        }\n        this.viewObjects = viewObjects;\n    }\n\n    /**\n     * Format delay to readable.\n     *\n     * @private\n     * @param {number} delay\n     * @returns {string}\n     */\n    _formatDelay(delay) {\n        return delay ? (Math.ceil(delay * 10) / 10).toFixed(1) : \".\";\n    }\n\n    /**\n     * Starts the ace library on the given DOM element. This initializes the\n     * ace editor in readonly mode.\n     *\n     * @private\n     * @param {Node} node - the DOM element the ace library must initialize on\n     */\n    _startAce(node) {\n        this.aceEditor = window.ace.edit(node);\n        this.aceEditor.setOptions({\n            maxLines: Infinity,\n            showPrintMargin: false,\n            highlightActiveLine: false,\n            highlightGutterLine: true,\n            readOnly: true,\n        });\n        this.aceEditor.renderer.setOptions({\n            displayIndentGuides: true,\n            showGutter: true,\n        });\n        this.aceEditor.renderer.$cursorLayer.element.style.display = \"none\";\n\n        this.aceEditor.$blockScrolling = true;\n        this.aceSession = this.aceEditor.getSession();\n        this.aceSession.setOptions({\n            useWorker: false,\n            mode: \"ace/mode/qweb\",\n            tabSize: 2,\n            useSoftTabs: true,\n        });\n\n        // Ace render 3 times when change the value and 1 time per click.\n        this.aceEditor.renderer.on(\"afterRender\", this.renderProfilingInformation.bind(this));\n    }\n\n    renderProfilingInformation() {\n        this._unmoutInfo();\n\n        const flat = {};\n        const arch = [{ xpath: \"\", children: [] }];\n        const rows = this.ace.el.querySelectorAll(\".ace_gutter .ace_gutter-cell\");\n        const elems = this.ace.el.querySelectorAll(\n            \".ace_tag-open, .ace_end-tag-close, .ace_end-tag-open, .ace_qweb\"\n        );\n        elems.forEach((node) => {\n            const parent = arch[arch.length - 1];\n            let xpath = parent.xpath;\n            if (node.classList.contains(\"ace_end-tag-close\")) {\n                // Close tag.\n                let previous = node;\n                while ((previous = previous.previousElementSibling)) {\n                    if (previous && previous.classList.contains(\"ace_tag-name\")) {\n                        break;\n                    }\n                }\n                const tag = previous && previous.textContent;\n                if (parent.tag === tag) {\n                    // can be different when scroll because ace does not display the previous lines.\n                    arch.pop();\n                }\n            } else if (node.classList.contains(\"ace_end-tag-open\")) {\n                // Auto close tag.\n                const tag = node.nextElementSibling && node.nextElementSibling.textContent;\n                if (parent.tag === tag) {\n                    // can be different when scroll because ace does not display the previous lines.\n                    arch.pop();\n                }\n            } else if (node.classList.contains(\"ace_qweb\")) {\n                // QWeb attribute.\n                const directive = node.textContent;\n                parent.directive.push({\n                    el: node,\n                    directive: directive,\n                });\n\n                // Compute delay and query number.\n                let delay = 0;\n                let query = 0;\n                for (const line of this.profile.data) {\n                    if (\n                        line.view_id === this.state.viewID &&\n                        line.xpath === xpath &&\n                        line.directive.includes(directive)\n                    ) {\n                        delay += line.delay;\n                        query += line.query;\n                    }\n                }\n\n                // Render delay and query number in span visible on hover.\n                if ((delay || query) && !node.querySelector(\".o_info\")) {\n                    this._renderHover(delay, query, node);\n                }\n            } else if (node.classList.contains(\"ace_tag-open\")) {\n                // Open tag.\n                const nodeTagName = node.nextElementSibling;\n                const aceLine = nodeTagName.parentNode;\n                const index = [].indexOf.call(aceLine.parentNode.children, aceLine);\n                const row = rows[index];\n\n                // Add a children to the arch and compute the xpath.\n                xpath += \"/\" + nodeTagName.textContent;\n                let i = 1;\n                while (flat[xpath + \"[\" + i + \"]\"]) {\n                    i++;\n                }\n                xpath += \"[\" + i + \"]\";\n                flat[xpath] = {\n                    xpath: xpath,\n                    tag: nodeTagName.textContent,\n                    children: [],\n                    directive: [],\n                };\n                arch.push(flat[xpath]);\n                parent.children.push(flat[xpath]);\n\n                // Compute delay and query number.\n                const closed = !!row.querySelector(\".ace_closed\");\n                const delays = [];\n                const querys = [];\n                const groups = {};\n                let displayDetail = false;\n                for (const line of this.profile.data) {\n                    if (\n                        line.view_id === this.state.viewID &&\n                        (closed ? line.xpath.startsWith(xpath) : line.xpath === xpath)\n                    ) {\n                        delays.push(line.delay);\n                        querys.push(line.query);\n                        const directive = line.directive.split(\"=\")[0];\n                        if (!groups[directive]) {\n                            groups[directive] = {\n                                delays: [],\n                                querys: [],\n                            };\n                        } else {\n                            displayDetail = true;\n                        }\n                        groups[directive].delays.push(this._formatDelay(line.delay));\n                        groups[directive].querys.push(line.query);\n                    }\n                }\n\n                // Display delay and query number in front of the line.\n                if (delays.length && !row.querySelector(\".o_info\")) {\n                    this._renderInfo(delays, querys, displayDetail, groups, row);\n                }\n            }\n            node.setAttribute(\"data-xpath\", xpath);\n        });\n    }\n    /**\n     * Set the view ID and send atch to ACE.\n     *\n     * @private\n     */\n    _renderView() {\n        const view = this.viewObjects.find((view) => view.id === this.state.viewID);\n        if (view) {\n            const arch = this.profile.archs[view.id] || \"\";\n            if (this.aceSession.getValue() !== arch) {\n                this.aceSession.setValue(arch);\n            }\n        } else {\n            this.aceSession.setValue(\"\");\n        }\n        this.state.view = view;\n    }\n    _unmoutInfo() {\n        if (this.hover) {\n            if (this.ace.el.querySelector(\".o_ace_hover\")) {\n                this.ace.el.querySelector(\".o_ace_hover\").remove();\n            }\n        }\n        if (this.info) {\n            if (this.ace.el.querySelector(\".o_ace_info\")) {\n                this.ace.el.querySelector(\".o_ace_info\").remove();\n            }\n        }\n    }\n    _renderHover(delay, query, node) {\n        const xml = renderToString(\"web.ProfilingQwebView.hover\", {\n            delay: this._formatDelay(delay),\n            query: query,\n        });\n        const div = new DOMParser().parseFromString(xml, \"text/html\").querySelector(\"div\");\n        node.appendChild(div);\n    }\n    _renderInfo(delays, querys, displayDetail, groups, node) {\n        const xml = renderToString(\"web.ProfilingQwebView.info\", {\n            delay: this._formatDelay(delays.reduce((a, b) => a + b, 0)),\n            query: querys.reduce((a, b) => a + b, 0) || \".\",\n            displayDetail: displayDetail,\n            groups: groups,\n        });\n        const div = new DOMParser().parseFromString(xml, \"text/html\").querySelector(\"div\");\n        node.appendChild(div);\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {MouseEvent} ev\n     */\n    _onSelectView(ev) {\n        this.state.viewID = +ev.currentTarget.dataset.id;\n        this._renderView();\n    }\n}\n\nexport const profilingQwebView = {\n    component: ProfilingQwebView,\n};\n\nregistry.category(\"fields\").add(\"profiling_qweb_view\", profilingQwebView);\n", "import { registry } from \"@web/core/registry\";\nimport { ProfilingItem } from \"./profiling_item\";\nimport { session } from \"@web/session\";\nimport { profilingSystrayItem } from \"./profiling_systray_item\";\n\nimport { EventBus, reactive } from \"@odoo/owl\";\n\nconst systrayRegistry = registry.category(\"systray\");\n\nexport const profilingService = {\n    dependencies: [\"orm\"],\n    start(env, { orm }) {\n        // Only set up profiling when in debug mode\n        if (!env.debug) {\n            return;\n        }\n\n        function notify() {\n            if (systrayRegistry.contains(\"web.profiling\") && state.isEnabled === false) {\n                systrayRegistry.remove(\"web.profiling\");\n            }\n            if (!systrayRegistry.contains(\"web.profiling\") && state.isEnabled === true) {\n                systrayRegistry.add(\"web.profiling\", profilingSystrayItem, { sequence: 99 });\n            }\n            bus.trigger(\"UPDATE\");\n        }\n\n        const state = reactive(\n            {\n                session: session.profile_session || false,\n                collectors: session.profile_collectors || [\"sql\", \"traces_async\"],\n                params: session.profile_params || {},\n                get isEnabled() {\n                    return Boolean(state.session);\n                },\n            },\n            notify\n        );\n\n        const bus = new EventBus();\n        notify();\n\n        async function setProfiling(params) {\n            const kwargs = Object.assign(\n                {\n                    collectors: state.collectors,\n                    params: state.params,\n                    profile: state.isEnabled,\n                },\n                params\n            );\n            const resp = await orm.call(\"ir.profile\", \"set_profiling\", [], kwargs);\n            if (resp.type) {\n                // most likely an \"ir.actions.act_window\"\n                env.services.action.doAction(resp);\n            } else {\n                state.session = resp.session;\n                state.collectors = resp.collectors;\n                state.params = resp.params;\n            }\n        }\n\n        function profilingItem() {\n            return {\n                type: \"component\",\n                Component: ProfilingItem,\n                props: { bus },\n                sequence: 570,\n                section: \"tools\",\n            };\n        }\n\n        registry.category(\"debug\").category(\"default\").add(\"profilingItem\", profilingItem);\n\n        return {\n            state,\n            async toggleProfiling() {\n                await setProfiling({ profile: !state.isEnabled });\n            },\n            async toggleCollector(collector) {\n                const nextCollectors = state.collectors.slice();\n                const index = nextCollectors.indexOf(collector);\n                if (index >= 0) {\n                    nextCollectors.splice(index, 1);\n                } else {\n                    nextCollectors.push(collector);\n                }\n                await setProfiling({ collectors: nextCollectors });\n            },\n            async setParam(key, value) {\n                const nextParams = Object.assign({}, state.params);\n                nextParams[key] = value;\n                await setProfiling({ params: nextParams });\n            },\n            isCollectorEnabled(collector) {\n                return state.collectors.includes(collector);\n            },\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"profiling\", profilingService);\n", "import { Component } from \"@odoo/owl\";\n\nclass ProfilingSystrayItem extends Component {\n    static template = \"web.ProfilingSystrayItem\";\n    static props = {};\n}\n\nexport const profilingSystrayItem = {\n    Component: ProfilingSystrayItem,\n};\n", "import { browser } from \"@web/core/browser/browser\";\nimport { rpcBus } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { useBus } from \"@web/core/utils/hooks\";\nimport { Transition } from \"@web/core/transition\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\n/**\n * Loading Indicator\n *\n * When the user performs an action, it is good to give him some feedback that\n * something is currently happening.  The purpose of the Loading Indicator is to\n * display a small rectangle on the bottom right of the screen with just the\n * text 'Loading' and the number of currently running rpcs.\n *\n * After a delay of 3s, if a rpc is still not completed, we also block the UI.\n */\nexport class LoadingIndicator extends Component {\n    static template = \"web.LoadingIndicator\";\n    static components = { Transition };\n    static props = {};\n\n    setup() {\n        this.state = useState({\n            count: 0,\n            show: false,\n        });\n        this.rpcIds = new Set();\n        this.startShowTimer = null;\n        useBus(rpcBus, \"RPC:REQUEST\", this.requestCall);\n        useBus(rpcBus, \"RPC:RESPONSE\", this.responseCall);\n    }\n\n    requestCall({ detail }) {\n        if (detail.settings.silent) {\n            return;\n        }\n        if (this.state.count === 0) {\n            browser.clearTimeout(this.startShowTimer);\n            this.startShowTimer = browser.setTimeout(() => {\n                if (this.state.count) {\n                    this.state.show = true;\n                }\n            }, 250);\n        }\n        this.rpcIds.add(detail.data.id);\n        this.state.count++;\n    }\n\n    responseCall({ detail }) {\n        if (detail.settings.silent) {\n            return;\n        }\n        this.rpcIds.delete(detail.data.id);\n        this.state.count = this.rpcIds.size;\n        if (this.state.count === 0) {\n            browser.clearTimeout(this.startShowTimer);\n            this.state.show = false;\n        }\n    }\n}\n\nregistry.category(\"main_components\").add(\"LoadingIndicator\", {\n    Component: LoadingIndicator,\n});\n", "/**\n * Traverses the given menu tree, executes the given callback for each node with\n * the node itself and the list of its ancestors as arguments.\n *\n * @param {Object} tree tree of menus as exported by the menus service\n * @param {Function} cb\n * @param {[Object]} [parents] the ancestors of the tree root, if any\n */\nfunction traverseMenuTree(tree, cb, parents = []) {\n    cb(tree, parents);\n    tree.childrenTree.forEach((c) => traverseMenuTree(c, cb, parents.concat([tree])));\n}\n\n/**\n * Computes the \"apps\" and \"menuItems\" from a given menu tree.\n *\n * @param {Object} menuTree tree of menus as exported by the menus service\n * @returns {Object} with keys \"apps\" and \"menuItems\" (HomeMenu props)\n */\nexport function computeAppsAndMenuItems(menuTree) {\n    const apps = [];\n    const menuItems = [];\n    traverseMenuTree(menuTree, (menuItem, parents) => {\n        if (!menuItem.id || !menuItem.actionID) {\n            return;\n        }\n        const isApp = menuItem.id === menuItem.appID;\n        const item = {\n            parents: parents\n                .slice(1)\n                .map((p) => p.name)\n                .join(\" / \"),\n            label: menuItem.name,\n            id: menuItem.id,\n            xmlid: menuItem.xmlid,\n            actionID: menuItem.actionID,\n            href: `/odoo/${menuItem.actionPath || \"action-\" + menuItem.actionID}`,\n            appID: menuItem.appID,\n        };\n        if (isApp) {\n            if (menuItem.webIconData) {\n                item.webIconData = menuItem.webIconData;\n            } else {\n                const [iconClass, color, backgroundColor] = (menuItem.webIcon || \"\").split(\",\");\n                if (backgroundColor !== undefined) {\n                    // Could split in three parts?\n                    item.webIcon = { iconClass, color, backgroundColor };\n                } else {\n                    item.webIconData = \"/web/static/img/default_icon_app.png\";\n                }\n            }\n        } else {\n            item.menuID = parents[1].id;\n        }\n        if (isApp) {\n            apps.push(item);\n        } else {\n            menuItems.push(item);\n        }\n    });\n    return { apps, menuItems };\n}\n\n/**\n * @param {Array} order\n * Sorts the apps in the homescreen menu according to the given order as an array of xmlid strings\n */\nexport function reorderApps(apps, order) {\n    apps.sort((a, b) => {\n        const aIndex = order.indexOf(a.xmlid);\n        const bIndex = order.indexOf(b.xmlid);\n        if (aIndex === -1 && bIndex === -1) {\n            // if both items are not present, sort by original order\n            return apps.indexOf(a) - apps.indexOf(b);\n        }\n        // not found items always before found ones\n        if (aIndex === -1) {\n            return -1;\n        }\n        if (bIndex === -1) {\n            return 1;\n        }\n        return aIndex - bIndex; // sort by order array\n    });\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { fuzzyLookup } from \"@web/core/utils/search\";\nimport { computeAppsAndMenuItems } from \"@web/webclient/menus/menu_helpers\";\nimport { DefaultCommandItem } from \"@web/core/commands/command_palette\";\n\nimport { Component } from \"@odoo/owl\";\n\nclass AppIconCommand extends Component {\n    static template = \"web.AppIconCommand\";\n    static props = {\n        webIconData: { type: String, optional: true },\n        webIcon: { type: Object, optional: true },\n        ...DefaultCommandItem.props,\n    };\n}\n\nconst commandCategoryRegistry = registry.category(\"command_categories\");\ncommandCategoryRegistry.add(\"apps\", { namespace: \"/\" }, { sequence: 10 });\ncommandCategoryRegistry.add(\"menu_items\", { namespace: \"/\" }, { sequence: 20 });\n\nconst commandSetupRegistry = registry.category(\"command_setup\");\ncommandSetupRegistry.add(\"/\", {\n    emptyMessage: _t(\"No menu found\"),\n    name: _t(\"menus\"),\n    placeholder: _t(\"Search for a menu...\"),\n});\n\nconst commandProviderRegistry = registry.category(\"command_provider\");\ncommandProviderRegistry.add(\"menu\", {\n    namespace: \"/\",\n    async provide(env, options) {\n        const result = [];\n        const menuService = env.services.menu;\n        let { apps, menuItems } = computeAppsAndMenuItems(menuService.getMenuAsTree(\"root\"));\n        if (options.searchValue !== \"\") {\n            apps = fuzzyLookup(options.searchValue, apps, (menu) => menu.label);\n\n            fuzzyLookup(options.searchValue, menuItems, (menu) =>\n                (menu.parents + \" / \" + menu.label).split(\"/\").reverse().join(\"/\")\n            ).forEach((menu) => {\n                result.push({\n                    action() {\n                        menuService.selectMenu(menu);\n                    },\n                    category: \"menu_items\",\n                    name: menu.parents + \" / \" + menu.label,\n                    href: menu.href || `#menu_id=${menu.id}&amp;action_id=${menu.actionID}`,\n                });\n            });\n        }\n\n        apps.forEach((menu) => {\n            const props = {};\n            if (menu.webIconData) {\n                const prefix = menu.webIconData.startsWith(\"P\")\n                    ? \"data:image/svg+xml;base64,\"\n                    : \"data:image/png;base64,\";\n                props.webIconData = menu.webIconData.startsWith(\"data:image\")\n                    ? menu.webIconData\n                    : prefix + menu.webIconData.replace(/\\s/g, \"\");\n            } else {\n                props.webIcon = menu.webIcon;\n            }\n            result.push({\n                Component: AppIconCommand,\n                action() {\n                    menuService.selectMenu(menu);\n                },\n                category: \"apps\",\n                name: menu.label,\n                href: menu.href || `#menu_id=${menu.id}&amp;action_id=${menu.actionID}`,\n                props,\n            });\n        });\n\n        return result;\n    },\n});\n", "import { browser } from \"../../core/browser/browser\";\nimport { registry } from \"../../core/registry\";\nimport { session } from \"@web/session\";\n\nconst loadMenusUrl = `/web/webclient/load_menus`;\n\nfunction makeFetchLoadMenus() {\n    const cacheHashes = session.cache_hashes;\n    let loadMenusHash = cacheHashes.load_menus || new Date().getTime().toString();\n    return async function fetchLoadMenus(reload) {\n        if (reload) {\n            loadMenusHash = new Date().getTime().toString();\n        } else if (odoo.loadMenusPromise) {\n            return odoo.loadMenusPromise;\n        }\n        const res = await browser.fetch(`${loadMenusUrl}/${loadMenusHash}`);\n        if (!res.ok) {\n            throw new Error(\"Error while fetching menus\");\n        }\n        return res.json();\n    };\n}\n\nfunction makeMenus(env, menusData, fetchLoadMenus) {\n    let currentAppId;\n    function _getMenu(menuId) {\n        return menusData[menuId];\n    }\n    function setCurrentMenu(menu) {\n        menu = typeof menu === \"number\" ? _getMenu(menu) : menu;\n        if (menu && menu.appID !== currentAppId) {\n            currentAppId = menu.appID;\n            browser.sessionStorage.setItem(\"menu_id\", currentAppId);\n            env.bus.trigger(\"MENUS:APP-CHANGED\");\n        }\n    }\n\n    return {\n        getAll() {\n            return Object.values(menusData);\n        },\n        getApps() {\n            return this.getMenu(\"root\").children.map((mid) => this.getMenu(mid));\n        },\n        getMenu: _getMenu,\n        getCurrentApp() {\n            if (!currentAppId) {\n                return;\n            }\n            return this.getMenu(currentAppId);\n        },\n        getMenuAsTree(menuID) {\n            const menu = this.getMenu(menuID);\n            if (!menu.childrenTree) {\n                menu.childrenTree = menu.children.map((mid) => this.getMenuAsTree(mid));\n            }\n            return menu;\n        },\n        async selectMenu(menu) {\n            menu = typeof menu === \"number\" ? this.getMenu(menu) : menu;\n            if (!menu.actionID) {\n                return;\n            }\n            await env.services.action.doAction(menu.actionID, {\n                clearBreadcrumbs: true,\n                onActionReady: () => {\n                    setCurrentMenu(menu);\n                },\n            });\n        },\n        setCurrentMenu,\n        async reload() {\n            if (fetchLoadMenus) {\n                menusData = await fetchLoadMenus(true);\n                env.bus.trigger(\"MENUS:APP-CHANGED\");\n            }\n        },\n    };\n}\n\nexport const menuService = {\n    dependencies: [\"action\"],\n    async start(env) {\n        const fetchLoadMenus = makeFetchLoadMenus();\n        const menusData = await fetchLoadMenus();\n        return makeMenus(env, menusData, fetchLoadMenus);\n    },\n};\n\nregistry.category(\"services\").add(\"menu\", menuService);\n", "import { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { DropdownGroup } from \"@web/core/dropdown/dropdown_group\";\nimport { Transition } from \"@web/core/transition\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { ErrorHandler } from \"@web/core/utils/components\";\n\nimport {\n    Component,\n    onWillDestroy,\n    useExternalListener,\n    useEffect,\n    useRef,\n    useState,\n    onWillUnmount,\n} from \"@odoo/owl\";\nconst systrayRegistry = registry.category(\"systray\");\n\nconst getBoundingClientRect = Element.prototype.getBoundingClientRect;\n\nconst SWIPE_ACTIVATION_THRESHOLD = 100;\n\nexport class MenuDropdown extends Dropdown {}\n\nexport class NavBar extends Component {\n    static template = \"web.NavBar\";\n    static components = { Dropdown, DropdownItem, DropdownGroup, MenuDropdown, ErrorHandler, Transition };\n    static props = {};\n\n    setup() {\n        this.currentAppSectionsExtra = [];\n        this.actionService = useService(\"action\");\n        this.menuService = useService(\"menu\");\n        this.pwa = useService(\"pwa\");\n        this.root = useRef(\"root\");\n        this.appSubMenus = useRef(\"appSubMenus\");\n        const debouncedAdapt = debounce(this.adapt.bind(this), 250);\n        onWillDestroy(() => debouncedAdapt.cancel());\n        useExternalListener(window, \"resize\", debouncedAdapt);\n\n        let adaptCounter = 0;\n        const renderAndAdapt = () => {\n            adaptCounter++;\n            this.render();\n        };\n\n        systrayRegistry.addEventListener(\"UPDATE\", renderAndAdapt);\n        this.env.bus.addEventListener(\"MENUS:APP-CHANGED\", renderAndAdapt);\n\n        onWillUnmount(() => {\n            systrayRegistry.removeEventListener(\"UPDATE\", renderAndAdapt);\n            this.env.bus.removeEventListener(\"MENUS:APP-CHANGED\", renderAndAdapt);\n        });\n\n        // We don't want to adapt every time we are patched\n        // rather, we adapt only when menus or systrays have changed.\n        useEffect(\n            () => {\n                this.adapt();\n            },\n            () => [adaptCounter]\n        );\n\n        this.state = useState({\n            isAllAppsMenuOpened: false,\n            isAppMenuSidebarOpened: false,\n        });\n    }\n\n    handleItemError(error, item) {\n        // remove the faulty component\n        item.isDisplayed = () => false;\n        Promise.resolve().then(() => {\n            throw error;\n        });\n    }\n\n    get currentApp() {\n        return this.menuService.getCurrentApp();\n    }\n\n    get currentAppSections() {\n        return (\n            (this.currentApp && this.menuService.getMenuAsTree(this.currentApp.id).childrenTree) ||\n            []\n        );\n    }\n\n    // This dummy setter is only here to prevent conflicts between the\n    // Enterprise NavBar extension and the Website NavBar patch.\n    set currentAppSections(_) {}\n\n    get isScopedApp() {\n        return this.pwa.isScopedApp;\n    }\n\n    get systrayItems() {\n        return systrayRegistry\n            .getEntries()\n            .map(([key, value]) => ({ key, ...value }))\n            .filter((item) => (\"isDisplayed\" in item ? item.isDisplayed(this.env) : true))\n            .reverse();\n    }\n\n    // This dummy setter is only here to prevent conflicts between the\n    // Enterprise NavBar extension and the Website NavBar patch.\n    set systrayItems(_) {}\n\n    /**\n     * Adapt will check the available width for the app sections to get displayed.\n     * If not enough space is available, it will replace by a \"more\" menu\n     * the least amount of app sections needed trying to fit the width.\n     *\n     * NB: To compute the widths of the actual app sections, a render needs to be done upfront.\n     *     By the end of this method another render may occur depending on the adaptation result.\n     */\n    async adapt() {\n        if (!this.root.el) {\n            /** @todo do we still need this check? */\n            // currently, the promise returned by 'render' is resolved at the end of\n            // the rendering even if the component has been destroyed meanwhile, so we\n            // may get here and have this.el unset\n            return;\n        }\n\n        // ------- Initialize -------\n        // Get the sectionsMenu\n        const sectionsMenu = this.appSubMenus.el;\n        if (!sectionsMenu) {\n            // No need to continue adaptations if there is no sections menu.\n            return;\n        }\n\n        // Save initial state to further check if new render has to be done.\n        const initialAppSectionsExtra = this.currentAppSectionsExtra;\n        const firstInitialAppSectionExtra = [...initialAppSectionsExtra].shift();\n        const initialAppId = firstInitialAppSectionExtra && firstInitialAppSectionExtra.appID;\n\n        // Restore (needed to get offset widths)\n        const sections = [\n            ...sectionsMenu.querySelectorAll(\":scope > *:not(.o_menu_sections_more)\"),\n        ];\n        for (const section of sections) {\n            section.classList.remove(\"d-none\");\n        }\n        this.currentAppSectionsExtra = [];\n\n        // ------- Check overflowing sections -------\n        // use getBoundingClientRect to get unrounded values for width in order to avoid rounding problem\n        // with offsetWidth.\n        const sectionsAvailableWidth = getBoundingClientRect.call(sectionsMenu).width;\n        const sectionsTotalWidth = sections.reduce(\n            (sum, s) => sum + getBoundingClientRect.call(s).width,\n            0\n        );\n        if (sectionsAvailableWidth < sectionsTotalWidth) {\n            // Sections are overflowing\n            // Initial width is harcoded to the width the more menu dropdown will take\n            let width = 46;\n            for (const section of sections) {\n                if (sectionsAvailableWidth < width + section.offsetWidth) {\n                    // Last sections are overflowing\n                    const overflowingSections = sections.slice(sections.indexOf(section));\n                    overflowingSections.forEach((s) => {\n                        // Hide from normal menu\n                        s.classList.add(\"d-none\");\n                        // Show inside \"more\" menu\n                        const sectionId =\n                            s.dataset.section ||\n                            s.querySelector(\"[data-section]\").getAttribute(\"data-section\");\n                        const currentAppSection = this.currentAppSections.find(\n                            (appSection) => appSection.id.toString() === sectionId\n                        );\n                        this.currentAppSectionsExtra.push(currentAppSection);\n                    });\n                    break;\n                }\n                width += section.offsetWidth;\n            }\n        }\n\n        // ------- Final rendering -------\n        const firstCurrentAppSectionExtra = [...this.currentAppSectionsExtra].shift();\n        const currentAppId = firstCurrentAppSectionExtra && firstCurrentAppSectionExtra.appID;\n        if (\n            initialAppSectionsExtra.length === this.currentAppSectionsExtra.length &&\n            initialAppId === currentAppId\n        ) {\n            // Do not render if more menu items stayed the same.\n            return;\n        }\n        return this.render();\n    }\n\n    onNavBarDropdownItemSelection(menu) {\n        if (menu) {\n            this.menuService.selectMenu(menu);\n        }\n    }\n\n    getMenuItemHref(payload) {\n        return `/odoo/${payload.actionPath || \"action-\" + payload.actionID}`;\n    }\n\n    _closeAppMenuSidebar() {\n        this.state.isAllAppsMenuOpened = false;\n        this.state.isAppMenuSidebarOpened = false;\n    }\n    _openAppMenuSidebar() {\n        this.state.isAppMenuSidebarOpened = !this.state.isAppMenuSidebarOpened;\n    }\n    onAllAppsBtnClick() {\n        this.state.isAllAppsMenuOpened = !this.state.isAllAppsMenuOpened;\n    }\n    async _onMenuClicked(menu) {\n        await this.menuService.selectMenu(menu);\n        this._closeAppMenuSidebar();\n    }\n    _onSwipeStart(ev) {\n        this.swipeStartX = ev.changedTouches[0].clientX;\n    }\n    _onSwipeEnd(ev) {\n        if (!this.swipeStartX) {\n            return;\n        }\n        const deltaX = this.swipeStartX - ev.changedTouches[0].clientX;\n        if (deltaX < SWIPE_ACTIVATION_THRESHOLD) {\n            return;\n        }\n        this._closeAppMenuSidebar();\n        this.swipeStartX = null;\n    }\n}\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { BinaryField, binaryField } from \"@web/views/fields/binary/binary_field\";\n\nexport class SettingsBinaryField extends BinaryField {\n    static template = \"web.SettingsBinaryField\";\n\n    getDownloadData() {\n        const related = this.props.record.fields[this.props.name].related;\n        return {\n            ...super.getDownloadData(),\n            model: this.props.record.fields[related.split(\".\")[0]].relation,\n            field: related.split(\".\")[1] ?? related.split(\".\")[0],\n            id: this.props.record.data[related.split(\".\")[0]][0],\n        }\n    }\n\n}\n\nconst settingsBinaryField = {\n    ...binaryField,\n    component: SettingsBinaryField,\n};\n\nregistry.category(\"fields\").add(\"base_settings.binary\", settingsBinaryField);\n", "import { registry } from \"@web/core/registry\";\nimport { booleanField, BooleanField } from \"@web/views/fields/boolean/boolean_field\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { UpgradeDialog } from \"./upgrade_dialog\";\n\n/**\n *  The upgrade boolean field is intended to be used in config settings.\n *  When checked, an upgrade popup is showed to the user.\n */\n\nexport class UpgradeBooleanField extends BooleanField {\n    setup() {\n        super.setup();\n        this.dialogService = useService(\"dialog\");\n        this.isEnterprise = odoo.info && odoo.info.isEnterprise;\n    }\n\n    async onChange(newValue) {\n        if (!this.isEnterprise) {\n            this.dialogService.add(\n                UpgradeDialog,\n                {},\n                {\n                    onClose: () => {\n                        this.props.record.update({ [this.props.name]: false });\n                    },\n                }\n            );\n        } else {\n            super.onChange(...arguments);\n        }\n    }\n}\n\nexport const upgradeBooleanField = {\n    ...booleanField,\n    component: UpgradeBooleanField,\n    additionalClasses: [...(booleanField.additionalClasses || []), \"o_field_boolean\"],\n};\n\nregistry.category(\"fields\").add(\"upgrade_boolean\", upgradeBooleanField);\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class UpgradeDialog extends Component {\n    static template = \"web.UpgradeDialog\";\n    static components = { Dialog };\n    static props = {\n        close: Function,\n    };\n    setup() {\n        this.orm = useService(\"orm\");\n    }\n    async _confirmUpgrade() {\n        const usersCount = await this.orm.call(\"res.users\", \"search_count\", [\n            [[\"share\", \"=\", false]],\n        ]);\n        window.open(\n            \"https://www.odoo.com/odoo-enterprise/upgrade?num_users=\" + usersCount,\n            \"_blank\"\n        );\n        this.props.close();\n    }\n}\n", "import { FormLabel } from \"@web/views/form/form_label\";\nimport { HighlightText } from \"./highlight_text\";\nimport { upgradeBooleanField } from \"../fields/upgrade_boolean_field\";\n\nexport class FormLabelHighlightText extends FormLabel {\n    static template = \"web.FormLabelHighlightText\";\n    static components = { HighlightText };\n    setup() {\n        super.setup();\n        const isEnterprise = odoo.info && odoo.info.isEnterprise;\n        if (this.props.fieldInfo?.field === upgradeBooleanField && !isEnterprise) {\n            this.upgradeEnterprise = true;\n        }\n    }\n}\n", "import { escapeRegExp } from \"@web/core/utils/strings\";\n\nimport { Component, useState, onWillRender } from \"@odoo/owl\";\n\nexport class HighlightText extends Component {\n    static template = \"web.HighlightText\";\n    static highlightClass = \"highlighter\";\n    static props = {\n        originalText: String,\n    };\n    setup() {\n        this.searchState = useState(this.env.searchState);\n\n        onWillRender(() => {\n            const splitText = this.props.originalText.split(\n                new RegExp(`(${escapeRegExp(this.searchState.value)})`, \"ig\")\n            );\n            this.splitText =\n                this.searchState.value.length && splitText.length > 1\n                    ? splitText\n                    : [this.props.originalText];\n        });\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { radioField, RadioField } from \"@web/views/fields/radio/radio_field\";\nimport { HighlightText } from \"./highlight_text\";\n\nexport class SettingsRadioField extends RadioField {\n    static template = \"web.SettingsRadioField\";\n    static components = { ...super.components, HighlightText };\n}\n\nexport const settingsRadioField = {\n    ...radioField,\n    component: SettingsRadioField,\n};\n\nregistry.category(\"fields\").add(\"base_settings.radio\", settingsRadioField);\n", "import { escapeRegExp } from \"@web/core/utils/strings\";\nimport { Setting } from \"@web/views/form/setting/setting\";\nimport { onMounted, useRef, useState } from \"@odoo/owl\";\nimport { FormLabelHighlightText } from \"../highlight_text/form_label_highlight_text\";\nimport { HighlightText } from \"../highlight_text/highlight_text\";\nimport { browser } from \"@web/core/browser/browser\";\n\nexport class SearchableSetting extends Setting {\n    static template = \"web.SearchableSetting\";\n    static components = {\n        ...Setting.components,\n        FormLabel: FormLabelHighlightText,\n        HighlightText,\n    };\n    setup() {\n        this.settingRef = useRef(\"setting\");\n        this.state = useState({\n            search: this.env.searchState,\n            showAllContainer: this.env.showAllContainer,\n            highlightClass: {},\n        });\n        this.labels = [];\n        this.labels.push(this.labelString, this.props.help);\n        super.setup();\n        onMounted(() => {\n            if (this.settingRef.el) {\n                const searchableTexts = this.settingRef.el.querySelectorAll(\"span[searchableText]\");\n                searchableTexts.forEach((st) => {\n                    this.labels.push(st.getAttribute(\"searchableText\"));\n                });\n            }\n            if (browser.location.hash.substring(1) === this.props.id) {\n                this.state.highlightClass = { o_setting_highlight: true };\n                setTimeout(() => (this.state.highlightClass = {}), 5000);\n            }\n        });\n    }\n\n    get classNames() {\n        const classNames = super.classNames;\n        classNames.o_searchable_setting = Boolean(this.labels.length);\n        return { ...classNames, ...this.state.highlightClass };\n    }\n\n    visible() {\n        if (!this.state.search.value) {\n            return true;\n        }\n        if (this.state.showAllContainer.showAllContainer) {\n            return true;\n        }\n        const regexp = new RegExp(escapeRegExp(this.state.search.value), \"i\");\n        if (regexp.test(this.labels.join())) {\n            return true;\n        }\n        return false;\n    }\n}\n", "import { Setting } from \"@web/views/form/setting/setting\";\n\nexport class SettingHeader extends Setting {\n    static template = \"web.HeaderSetting\";\n    get labelString() {\n        return this.props.string || this.props.record.fields[this.props.name].string;\n    }\n}\n", "import { Component, useState, useEffect, useRef } from \"@odoo/owl\";\n\nexport class SettingsApp extends Component {\n    static template = \"web.SettingsApp\";\n    static props = {\n        string: String,\n        imgurl: String,\n        key: String,\n        selectedTab: { type: String, optional: 1 },\n        slots: Object,\n    };\n    setup() {\n        this.state = useState({\n            search: this.env.searchState,\n        });\n        this.settingsAppRef = useRef(\"settingsApp\");\n        useEffect(\n            () => {\n                if (this.settingsAppRef.el) {\n                    const force =\n                        this.state.search.value &&\n                        !this.settingsAppRef.el.querySelector(\n                            \".o_settings_container:not(.d-none)\"\n                        ) &&\n                        !this.settingsAppRef.el.querySelector(\n                            \".o_setting_box.o_searchable_setting\"\n                        );\n                    this.settingsAppRef.el.classList.toggle(\"d-none\", force);\n                }\n            },\n            () => [this.state.search.value]\n        );\n    }\n}\n", "import { HighlightText } from \"../highlight_text/highlight_text\";\nimport { escapeRegExp } from \"@web/core/utils/strings\";\n\nimport { Component, useState, useRef, useEffect, onWillRender, useChildSubEnv } from \"@odoo/owl\";\n\nexport class SettingsBlock extends Component {\n    static template = \"web.SettingsBlock\";\n    static components = {\n        HighlightText,\n    };\n    static props = {\n        title: { type: String, optional: true },\n        tip: { type: String, optional: true },\n        slots: { type: Object, optional: true },\n        class: { type: String, optional: true },\n    };\n    setup() {\n        this.state = useState({\n            search: this.env.searchState,\n        });\n        this.showAllContainerState = useState({\n            showAllContainer: false,\n        });\n        useChildSubEnv({\n            showAllContainer: this.showAllContainerState,\n        });\n        this.settingsContainerRef = useRef(\"settingsContainer\");\n        this.settingsContainerTitleRef = useRef(\"settingsContainerTitle\");\n        this.settingsContainerTipRef = useRef(\"settingsContainerTip\");\n        useEffect(\n            () => {\n                const regexp = new RegExp(escapeRegExp(this.state.search.value), \"i\");\n                const force =\n                    this.state.search.value &&\n                    !regexp.test([this.props.title, this.props.tip].join()) &&\n                    !this.settingsContainerRef.el.querySelector(\n                        \".o_setting_box.o_searchable_setting\"\n                    );\n                this.toggleContainer(force);\n            },\n            () => [this.state.search.value]\n        );\n        onWillRender(() => {\n            const regexp = new RegExp(escapeRegExp(this.state.search.value), \"i\");\n            if (regexp.test([this.props.title, this.props.tip].join())) {\n                this.showAllContainerState.showAllContainer = true;\n            } else {\n                this.showAllContainerState.showAllContainer = false;\n            }\n        });\n    }\n    toggleContainer(force) {\n        if (this.settingsContainerTitleRef.el) {\n            this.settingsContainerTitleRef.el.classList.toggle(\"d-none\", force);\n        }\n        if (this.settingsContainerTipRef.el) {\n            this.settingsContainerTipRef.el.classList.toggle(\"d-none\", force);\n        }\n        this.settingsContainerRef.el.classList.toggle(\"d-none\", force);\n    }\n}\n", "import { ActionSwiper } from \"@web/core/action_swiper/action_swiper\";\n\nimport { Component, useState, useRef, useEffect } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\n\nexport class SettingsPage extends Component {\n    static template = \"web.SettingsPage\";\n    static components = { ActionSwiper };\n    static props = {\n        modules: Array,\n        anchors: Array,\n        initialTab: { type: String, optional: 1 },\n        slots: Object,\n    };\n    setup() {\n        this.state = useState({\n            selectedTab: \"\",\n            search: this.env.searchState,\n        });\n\n        if (this.props.modules) {\n            let selectedTab = this.props.initialTab || this.props.modules[0].key;\n\n            if (browser.location.hash) {\n                const hash = browser.location.hash.substring(1);\n                if (this.props.modules.map((m) => m.key).includes(hash)) {\n                    selectedTab = hash;\n                } else {\n                    const plop = this.props.anchors.find((a) => a.settingId === hash);\n                    if (plop) {\n                        selectedTab = plop.app;\n                    }\n                }\n            }\n\n            this.state.selectedTab = selectedTab;\n        }\n\n        this.settingsRef = useRef(\"settings\");\n        this.settingsTabRef = useRef(\"settings_tab\");\n        this.scrollMap = Object.create(null);\n        useEffect(\n            (settingsEl, currentTab) => {\n                if (!settingsEl) {\n                    return;\n                }\n\n                const { scrollTop } = this.scrollMap[currentTab] || 0;\n                settingsEl.scrollTop = scrollTop;\n            },\n            () => [this.settingsRef.el, this.state.selectedTab]\n        );\n    }\n\n    getCurrentIndex() {\n        return this.props.modules.findIndex((object) => {\n            return object.key === this.state.selectedTab;\n        });\n    }\n\n    hasRightSwipe() {\n        return (\n            this.env.isSmall && this.state.search.value.length === 0 && this.getCurrentIndex() !== 0\n        );\n    }\n    hasLeftSwipe() {\n        return (\n            this.env.isSmall &&\n            this.state.search.value.length === 0 &&\n            this.getCurrentIndex() !== this.props.modules.length - 1\n        );\n    }\n    async onRightSwipe(prom) {\n        this.state.selectedTab = this.props.modules[this.getCurrentIndex() - 1].key;\n        await prom;\n        this.scrollToSelectedTab();\n    }\n    async onLeftSwipe(prom) {\n        this.state.selectedTab = this.props.modules[this.getCurrentIndex() + 1].key;\n        await prom;\n        this.scrollToSelectedTab();\n    }\n\n    scrollToSelectedTab() {\n        const key = this.state.selectedTab;\n        this.settingsTabRef.el\n            .querySelector(`[data-key='${key}']`)\n            .scrollIntoView({ behavior: \"smooth\", inline: \"center\", block: \"nearest\" });\n    }\n\n    onSettingTabClick(key) {\n        if (this.settingsRef.el) {\n            const { scrollTop } = this.settingsRef.el;\n            this.scrollMap[this.state.selectedTab] = { scrollTop };\n        }\n        this.state.selectedTab = key;\n        this.env.searchState.value = \"\";\n    }\n}\n", "import { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class SettingsConfirmationDialog extends ConfirmationDialog {\n    static template = \"web.SettingsConfirmationDialog\";\n    static defaultProps = {\n        title: _t(\"Unsaved changes\"),\n    };\n    static props = {\n        ...ConfirmationDialog.props,\n        stayHere: { type: Function, optional: true },\n    };\n\n    _stayHere() {\n        if (this.props.stayHere) {\n            this.props.stayHere();\n        }\n        this.props.close();\n    }\n}\n", "import { append, createElement } from \"@web/core/utils/xml\";\nimport { FormCompiler } from \"@web/views/form/form_compiler\";\nimport { toStringExpression } from \"@web/views/utils\";\nimport { isTextNode } from \"@web/views/view_compiler\";\n\nexport class SettingsFormCompiler extends FormCompiler {\n    setup() {\n        super.setup();\n        this.compilers.push(\n            { selector: \"app\", fn: this.compileApp },\n            { selector: \"block\", fn: this.compileBlock }\n        );\n    }\n\n    compileForm(el, params) {\n        const settingsPage = createElement(\"SettingsPage\");\n        settingsPage.setAttribute(\n            \"slots\",\n            \"{NoContentHelper:__comp__.props.slots.NoContentHelper}\"\n        );\n        settingsPage.setAttribute(\"initialTab\", \"__comp__.props.initialApp\");\n        settingsPage.setAttribute(\"t-slot-scope\", \"settings\");\n\n        //props\n        params.modules = [];\n        params.anchors = [];\n\n        const res = super.compileForm(...arguments);\n        res.classList.remove(\"o_form_nosheet\");\n\n        settingsPage.setAttribute(\"modules\", JSON.stringify(params.modules));\n\n        // Move the compiled content of the form inside the settingsPage\n        while (res.firstChild) {\n            append(settingsPage, res.firstChild);\n        }\n\n        settingsPage.setAttribute(\"anchors\", JSON.stringify(params.anchors));\n\n        append(res, settingsPage);\n\n        return res;\n    }\n\n    compileApp(el, params) {\n        if (el.getAttribute(\"notApp\") === \"1\") {\n            //An app noted with notApp=\"1\" is not rendered.\n\n            //This hack is used when a technical module defines settings, and we don't want to render\n            //the settings until the corresponding app is not installed.\n\n            // For example, when installing the module website_sale, the module sale is also installed,\n            // but we don't want to render its settings (notApp=\"1\").\n            // On the contrary, when sale_management is installed, the module sale is also installed\n            // but in this case we want to see its settings (notApp=\"0\").\n            return;\n        }\n        const module = {\n            key: el.getAttribute(\"name\"),\n            string: el.getAttribute(\"string\"),\n            imgurl:\n                el.getAttribute(\"logo\") ||\n                \"/\" + el.getAttribute(\"name\") + \"/static/description/icon.png\",\n        };\n        params.modules.push(module);\n        const settingsApp = createElement(\"SettingsApp\", {\n            key: toStringExpression(module.key),\n            string: toStringExpression(module.string || \"\"),\n            imgurl: toStringExpression(module.imgurl),\n            selectedTab: \"settings.selectedTab\",\n        });\n\n        for (const child of el.children) {\n            append(settingsApp, this.compileNode(child, params));\n        }\n\n        params.anchors.push(\n            ...[...settingsApp.querySelectorAll(\"SearchableSetting\")]\n                .filter((s) => s.id)\n                .map((s) => ({ app: module.key, settingId: s.id.replaceAll(\"`\", \"\") }))\n        );\n        return settingsApp;\n    }\n\n    compileBlock(el, params) {\n        const settingsContainer = createElement(\"SettingsBlock\", {\n            title: toStringExpression(el.getAttribute(\"title\") || \"\"),\n            tip: toStringExpression(el.getAttribute(\"help\") || \"\"),\n        });\n        for (const child of el.children) {\n            append(settingsContainer, this.compileNode(child, params));\n        }\n        return settingsContainer;\n    }\n\n    compileSetting(el, params) {\n        params.componentName =\n            el.getAttribute(\"type\") === \"header\" ? \"SettingHeader\" : \"SearchableSetting\";\n        const res = super.compileSetting(el, params);\n        return res;\n    }\n\n    compileNode(node, params, evalInvisible) {\n        if (isTextNode(node)) {\n            if (node.textContent.trim()) {\n                return createElement(\"HighlightText\", {\n                    originalText: toStringExpression(node.textContent),\n                });\n            }\n        }\n        return super.compileNode(node, params, evalInvisible);\n    }\n\n    compileButton(el, params) {\n        const res = super.compileButton(el, params);\n        if (res.hasAttribute(\"string\") && res.children.length === 0) {\n            const contentSlot = createElement(\"t\");\n            contentSlot.setAttribute(\"t-set-slot\", \"contents\");\n            const content = createElement(\"HighlightText\", {\n                originalText: res.getAttribute(\"string\"),\n            });\n            append(contentSlot, content);\n            append(res, contentSlot);\n        }\n        return res;\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { useAutofocus } from \"@web/core/utils/hooks\";\nimport { pick } from \"@web/core/utils/objects\";\nimport { formView } from \"@web/views/form/form_view\";\nimport { SettingsConfirmationDialog } from \"./settings_confirmation_dialog\";\nimport { SettingsFormRenderer } from \"./settings_form_renderer\";\n\nimport { useSubEnv, useState, useRef, useEffect } from \"@odoo/owl\";\n\nexport class SettingsFormController extends formView.Controller {\n    static template = \"web.SettingsFormView\";\n    static components = {\n        ...formView.Controller.components,\n        Renderer: SettingsFormRenderer,\n    };\n\n    setup() {\n        super.setup();\n        useAutofocus();\n        this.state = useState({ displayNoContent: false });\n        this.searchState = useState({ value: \"\" });\n        this.rootRef = useRef(\"root\");\n        this.canCreate = false;\n        useSubEnv({ searchState: this.searchState });\n        useEffect(\n            () => {\n                if (this.searchState.value) {\n                    if (\n                        this.rootRef.el.querySelector(\".o_settings_container:not(.d-none)\") ||\n                        this.rootRef.el.querySelector(\n                            \".settings .o_settings_container:not(.d-none) .o_setting_box.o_searchable_setting\"\n                        )\n                    ) {\n                        this.state.displayNoContent = false;\n                    } else {\n                        this.state.displayNoContent = true;\n                    }\n                } else {\n                    this.state.displayNoContent = false;\n                }\n            },\n            () => [this.searchState.value]\n        );\n        useEffect(() => {\n            if (this.env.__getLocalState__) {\n                this.env.__getLocalState__.remove(this);\n            }\n        });\n\n        this.initialApp = \"module\" in this.props.context ? this.props.context.module : \"\";\n    }\n\n    get modelParams() {\n        const headerFields = Object.values(this.archInfo.fieldNodes)\n            .filter((fieldNode) => fieldNode.options.isHeaderField)\n            .map((fieldNode) => fieldNode.name);\n        return {\n            ...super.modelParams,\n            headerFields,\n            onChangeHeaderFields: () => this._confirmSave(),\n        };\n    }\n\n    /**\n     * @override\n     */\n    async beforeExecuteActionButton(clickParams) {\n        if (clickParams.name === \"cancel\") {\n            return true;\n        }\n        if (\n            (await this.model.root.isDirty()) &&\n            ![\"execute\"].includes(clickParams.name) &&\n            !clickParams.noSaveDialog\n        ) {\n            return this._confirmSave();\n        } else {\n            return this.model.root.save();\n        }\n    }\n\n    displayName() {\n        return _t(\"Settings\");\n    }\n\n    async beforeLeave() {\n        const dirty = await this.model.root.isDirty();\n        if (dirty) {\n            return this._confirmSave();\n        }\n    }\n\n    //This is needed to avoid the auto save when unload\n    beforeUnload() {}\n\n    //This is needed to avoid the auto save when visibility change\n    beforeVisibilityChange() {}\n\n    async save() {\n        await this.env.onClickViewButton({\n            clickParams: {\n                name: \"execute\",\n                type: \"object\",\n            },\n            getResParams: () =>\n                pick(this.model.root, \"context\", \"evalContext\", \"resModel\", \"resId\", \"resIds\"),\n        });\n    }\n\n    discard() {\n        this.env.onClickViewButton({\n            clickParams: {\n                name: \"cancel\",\n                type: \"object\",\n                special: \"cancel\",\n            },\n            getResParams: () =>\n                pick(this.model.root, \"context\", \"evalContext\", \"resModel\", \"resId\", \"resIds\"),\n        });\n    }\n\n    async _confirmSave() {\n        let _continue = true;\n        await new Promise((resolve) => {\n            this.dialogService.add(SettingsConfirmationDialog, {\n                body: _t(\"Would you like to save your changes?\"),\n                confirm: async () => {\n                    await this.save();\n                    // It doesn't make sense to do the action of the button\n                    // as the res.config.settings `execute` method will trigger a reload.\n                    _continue = false;\n                    resolve();\n                },\n                cancel: async () => {\n                    await this.model.root.discard();\n                    await this.model.root.save();\n                    _continue = true;\n                    resolve();\n                },\n                stayHere: () => {\n                    _continue = false;\n                    resolve();\n                },\n            });\n        });\n        return _continue;\n    }\n}\n", "import { FormRenderer } from \"@web/views/form/form_renderer\";\nimport { FormLabelHighlightText } from \"./highlight_text/form_label_highlight_text\";\nimport { HighlightText } from \"./highlight_text/highlight_text\";\nimport { SearchableSetting } from \"./settings/searchable_setting\";\nimport { SettingHeader } from \"./settings/setting_header\";\nimport { SettingsBlock } from \"./settings/settings_block\";\nimport { SettingsApp } from \"./settings/settings_app\";\nimport { SettingsPage } from \"./settings/settings_page\";\n\nimport { useState } from \"@odoo/owl\";\n\nexport class SettingsFormRenderer extends FormRenderer {\n    static components = {\n        ...FormRenderer.components,\n        SearchableSetting,\n        SettingHeader,\n        SettingsBlock,\n        SettingsPage,\n        SettingsApp,\n        HighlightText,\n        FormLabel: FormLabelHighlightText,\n    };\n    static props = {\n        ...FormRenderer.props,\n        initialApp: String,\n        slots: Object,\n    };\n\n    setup() {\n        super.setup();\n        this.searchState = useState(this.env.searchState);\n    }\n\n    get shouldAutoFocus() {\n        return false;\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { evaluateExpr } from \"@web/core/py_js/py\";\nimport { intersection } from \"@web/core/utils/arrays\";\nimport { ControlPanel } from \"@web/search/control_panel/control_panel\";\nimport { formView } from \"@web/views/form/form_view\";\nimport { SettingsFormController } from \"./settings_form_controller\";\nimport { SettingsFormRenderer } from \"./settings_form_renderer\";\nimport { SettingsFormCompiler } from \"./settings_form_compiler\";\n\nclass SettingRecord extends formView.Model.Record {\n    _update(changes) {\n        const changedFields = Object.keys(changes);\n        let dirty = true;\n        if (intersection(changedFields, this.model._headerFields).length === changedFields.length) {\n            dirty = this.dirty;\n            if (this.dirty) {\n                this.model._onChangeHeaderFields().then(async (isDiscard) => {\n                    if (isDiscard) {\n                        await super._update(...arguments);\n                        this.dirty = false;\n                    } else {\n                        // We need to apply and then undo the changes\n                        // to force the field component to be render\n                        // and restore the previous value (like RadioField))\n                        const undoChanges = this._applyChanges(changes);\n                        undoChanges();\n                    }\n                });\n                return;\n            }\n        }\n        const prom = super._update(...arguments);\n        this.dirty = dirty;\n        return prom;\n    }\n}\n\nclass SettingModel extends formView.Model {\n    setup(params) {\n        super.setup(...arguments);\n        this._headerFields = params.headerFields;\n        this._onChangeHeaderFields = params.onChangeHeaderFields;\n    }\n    _getNextConfig() {\n        const nextConfig = super._getNextConfig(...arguments);\n        nextConfig.resId = false;\n        return nextConfig;\n    }\n}\nSettingModel.Record = SettingRecord;\n\nexport const settingsFormView = {\n    ...formView,\n    display: {},\n    buttonTemplate: \"web.SettingsFormView.Buttons\",\n    Model: SettingModel,\n    ControlPanel: ControlPanel,\n    Controller: SettingsFormController,\n    Compiler: SettingsFormCompiler,\n    Renderer: SettingsFormRenderer,\n    props: (genericProps, view) => {\n        [...genericProps.arch.querySelectorAll(\"setting[type='header'] field\")].forEach((el) => {\n            const options = evaluateExpr(el.getAttribute(\"options\") || \"{}\");\n            options.isHeaderField = true;\n            el.setAttribute(\"options\", JSON.stringify(options));\n        });\n        return formView.props(genericProps, view);\n    },\n};\n\nregistry.category(\"views\").add(\"base_settings\", settingsFormView);\n", "import { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\n\nexport const demoDataService = {\n    async start() {\n        let isDemoDataActiveProm;\n        return {\n            isDemoDataActive() {\n                if (!isDemoDataActiveProm) {\n                    isDemoDataActiveProm = rpc(\"/base_setup/demo_active\");\n                }\n                return isDemoDataActiveProm;\n            },\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"demo_data\", demoDataService);\n", "import { registry } from \"@web/core/registry\";\n\nimport { Component } from \"@odoo/owl\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { Setting } from \"@web/views/form/setting/setting\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\n\n/**\n * Widget in the settings that displays links to the mobile apps.\n * As a QRCode in Desktop mode, as an image in mobile mode.\n * Both are clickable links.\n */\nclass MobileAppsFunnel extends Component {\n    static template = \"web.MobileAppsFunnel\";\n    static components = {\n        Setting,\n    };\n    static props = { ...standardWidgetProps };\n    setup() {\n        this.iosAppstoreImagePath = isMobileOS()\n            ? \"/web/static/img/app_store.png\"\n            : \"/web/static/img/mobile_app_qrcode_ios.svg\";\n        this.androidAppstoreImagePath = isMobileOS()\n            ? \"/web/static/img/google_play.png\"\n            : \"/web/static/img/mobile_app_qrcode_android.svg\";\n    }\n}\n\nexport const mobileAppsFunnel = {\n    component: MobileAppsFunnel,\n};\n\nregistry.category(\"view_widgets\").add(\"mobile_apps_funnel\", mobileAppsFunnel);\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { SettingsBlock } from \"../settings/settings_block\";\nimport { Setting } from \"../../../views/form/setting/setting\";\n\nimport { Component, onWillStart } from \"@odoo/owl\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { router } from \"@web/core/browser/router\";\n\n/**\n * Widget in the settings that handles the \"Developer Tools\" section.\n * Can be used to enable/disable the debug modes.\n * Can be used to load the demo data.\n */\nexport class ResConfigDevTool extends Component {\n    static template = \"res_config_dev_tool\";\n    static components = {\n        SettingsBlock,\n        Setting,\n    };\n    static props = {\n        ...standardWidgetProps,\n    };\n\n    setup() {\n        this.isDebug = Boolean(odoo.debug);\n        this.isAssets = odoo.debug.includes(\"assets\");\n        this.isTests = odoo.debug.includes(\"tests\");\n\n        this.action = useService(\"action\");\n        this.demo = useService(\"demo_data\");\n\n        onWillStart(async () => {\n            this.isDemoDataActive = await this.demo.isDemoDataActive();\n        });\n    }\n\n    activateDebug(value) {\n        router.pushState({ debug: value }, { reload: true });\n    }\n\n    /**\n     * Forces demo data to be installed in a database without demo data installed.\n     */\n    onClickForceDemo() {\n        this.action.doAction(\"base.demo_force_install_action\");\n    }\n}\n\nexport const resConfigDevTool = {\n    component: ResConfigDevTool,\n};\n\nregistry.category(\"view_widgets\").add(\"res_config_dev_tool\", resConfigDevTool);\n", "import { registry } from \"@web/core/registry\";\nimport { session } from \"@web/session\";\nimport { Setting } from \"@web/views/form/setting/setting\";\n\nimport { Component } from \"@odoo/owl\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nconst { DateTime } = luxon;\n\n/**\n * Widget in the settings that handles a part of the \"About\" section.\n * Contains info about the odoo version, database expiration date and copyrights.\n */\nclass ResConfigEdition extends Component {\n    static template = \"res_config_edition\";\n    static components = { Setting };\n    static props = {\n        ...standardWidgetProps,\n    };\n\n    setup() {\n        this.serverVersion = session.server_version;\n        this.expirationDate = session.expiration_date\n            ? DateTime.fromSQL(session.expiration_date).toLocaleString(DateTime.DATE_FULL)\n            : DateTime.now().plus({ days: 30 }).toLocaleString(DateTime.DATE_FULL);\n    }\n}\n\nexport const resConfigEdition = {\n    component: ResConfigEdition,\n};\n\nregistry.category(\"view_widgets\").add(\"res_config_edition\", resConfigEdition);\n", "import { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { formatList } from \"@web/core/l10n/utils\";\nimport { unique } from \"@web/core/utils/arrays\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { Component, useState, onWillStart } from \"@odoo/owl\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\n\nclass ResConfigInviteUsers extends Component {\n    static template = \"res_config_invite_users\";\n    static props = {\n        ...standardWidgetProps,\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.invite = useService(\"user_invite\");\n        this.action = useService(\"action\");\n        this.notification = useService(\"notification\");\n\n        this.state = useState({\n            status: \"idle\", // idle, inviting\n            emails: \"\",\n            invite: null,\n        });\n\n        onWillStart(async () => {\n            this.state.invite = await this.invite.fetchData();\n        });\n    }\n\n    /**\n     * @param {string} email\n     * @returns {boolean} true if the given email address is valid\n     */\n    validateEmail(email) {\n        const re =\n            /^([a-z0-9][-a-z0-9_+.]*)@((?:[\\w-]+\\.)*\\w[\\w-]{0,66})\\.([a-z]{2,63}(?:\\.[a-z]{2})?)$/i;\n        return re.test(email);\n    }\n\n    get emails() {\n        return unique(\n            this.state.emails\n                .split(/[ ,;\\n]+/)\n                .map((email) => email.trim())\n                .filter((email) => email.length)\n        );\n    }\n\n    validate() {\n        if (!this.emails.length) {\n            throw new Error(_t(\"Empty email address\"));\n        }\n        const invalidEmails = [];\n        for (const email of this.emails) {\n            if (!this.validateEmail(email)) {\n                invalidEmails.push(email);\n            }\n        }\n        if (invalidEmails.length) {\n            const errorMessage = (() => {\n                switch (invalidEmails.length) {\n                    case 1:\n                        return _t(\"Invalid email address: %(address)s\", {\n                            address: invalidEmails[0],\n                        });\n                    case 2:\n                        return _t(\"Invalid email addresses: %(2 addresses)s\", {\n                            \"2 addresses\": formatList(invalidEmails),\n                        });\n                    default:\n                        return _t(\"Invalid email addresses: %(addresses)s\", {\n                            addresses: formatList(invalidEmails),\n                        });\n                }\n            })();\n            throw new Error(errorMessage);\n        }\n    }\n\n    get inviteButtonText() {\n        if (this.state.status === \"inviting\") {\n            return _t(\"Inviting...\");\n        }\n        return _t(\"Invite\");\n    }\n\n    onClickMore(ev) {\n        this.action.doAction(this.state.invite.action_pending_users);\n    }\n\n    onClickUser(ev, user) {\n        const action = Object.assign({}, this.state.invite.action_pending_users, {\n            res_id: user[0],\n        });\n        this.action.doAction(action);\n    }\n\n    onKeydownUserEmails(ev) {\n        const keys = [\"Enter\", \"Tab\", \",\"];\n        if (keys.includes(ev.key)) {\n            if (ev.key === \"Tab\" && !this.emails.length) {\n                return;\n            }\n            ev.preventDefault();\n            this.sendInvite();\n        }\n    }\n\n    /**\n     * Send invitation for valid and unique email addresses\n     *\n     * @private\n     */\n    async sendInvite() {\n        try {\n            this.validate();\n        } catch (e) {\n            this.notification.add(e.message, { type: \"danger\" });\n            return;\n        }\n\n        this.state.status = \"inviting\";\n\n        const pendingUserEmails = this.state.invite.pending_users.map((user) => user[1]);\n        const emailsLeftToProcess = this.emails.filter(\n            (email) => !pendingUserEmails.includes(email)\n        );\n\n        try {\n            if (emailsLeftToProcess) {\n                await this.orm.call(\"res.users\", \"web_create_users\", [emailsLeftToProcess]);\n                this.state.invite = await this.invite.fetchData(true);\n            }\n        } finally {\n            this.state.emails = \"\";\n            this.state.status = \"idle\";\n        }\n    }\n}\n\nexport const resConfigInviteUsers = {\n    component: ResConfigInviteUsers,\n};\n\nregistry.category(\"view_widgets\").add(\"res_config_invite_users\", resConfigInviteUsers);\n", "import { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\n\nexport const userInviteService = {\n    async start() {\n        let dataProm;\n        return {\n            fetchData(reload = false) {\n                if (!dataProm || reload) {\n                    dataProm = rpc(\"/base_setup/data\");\n                }\n                return dataProm;\n            },\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"user_invite\", userInviteService);\n", "import { registry } from \"@web/core/registry\";\nimport { browser } from \"@web/core/browser/browser\";\n\n/**\n * @return {Promise<{\n *     title:string,\n *     text:string,\n *     url:string,\n *     externalMediaFiles:File[]\n * }>}\n */\nconst getShareTargetDataFromServiceWorker = () => {\n    return new Promise((resolve) => {\n        const onmessage = (event) => {\n            if (event.data.action === \"odoo_share_target_ack\") {\n                resolve(event.data.shared_files);\n                browser.navigator.serviceWorker.removeEventListener(\"message\", onmessage);\n            }\n        };\n        browser.navigator.serviceWorker.addEventListener(\"message\", onmessage);\n        browser.navigator.serviceWorker.controller.postMessage(\"odoo_share_target\");\n    });\n};\n\nexport const shareTargetService = {\n    dependencies: [\"menu\"],\n    start(env, { menu }) {\n        let sharedFiles = null;\n        if (\n            browser.navigator.serviceWorker &&\n            new URL(browser.location).searchParams.get(\"share_target\") === \"trigger\"\n        ) {\n            const app = menu.getApps().find((app) => \"expenses\" === app.actionPath);\n            if (app) {\n                const clientReadyListener = async () => {\n                    sharedFiles = await getShareTargetDataFromServiceWorker();\n                    if (sharedFiles?.length) {\n                        await menu.selectMenu(app);\n                    }\n                    env.bus.removeEventListener(\"WEB_CLIENT_READY\", clientReadyListener);\n                };\n                env.bus.addEventListener(\"WEB_CLIENT_READY\", clientReadyListener);\n            }\n        }\n        return {\n            /**\n             * Return true if we receive share target files from service worker\n             * @return {boolean}\n             */\n            hasSharedFiles: () => !!sharedFiles?.length,\n            /**\n             * Return the shared files retrieve for upload\n             * @return {null|File[]}\n             */\n            getSharedFilesToUpload: () => {\n                const files = sharedFiles;\n                sharedFiles = null;\n                return files;\n            },\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"shareTarget\", shareTargetService);\n", "import { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { Component, useState } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class SwitchCompanyItem extends Component {\n    static template = \"web.SwitchCompanyItem\";\n    static components = { DropdownItem, SwitchCompanyItem };\n    static props = {\n        company: {},\n        level: { type: Number },\n    };\n\n    setup() {\n        this.companyService = useService(\"company\");\n        this.companySelector = useState(this.env.companySelector);\n    }\n\n    get isCompanySelected() {\n        return this.companySelector.isCompanySelected(this.props.company.id);\n    }\n\n    get isCompanyAllowed() {\n        return this.props.company.id in this.companyService.allowedCompanies;\n    }\n\n    get isCurrent() {\n        return this.props.company.id === this.companyService.currentCompany.id;\n    }\n\n    logIntoCompany() {\n        if (this.isCompanyAllowed) {\n            this.companySelector.switchCompany(\"loginto\", this.props.company.id);\n        }\n    }\n\n    toggleCompany() {\n        if (this.isCompanyAllowed) {\n            this.companySelector.switchCompany(\"toggle\", this.props.company.id);\n        }\n    }\n}\n", "import { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownGroup } from \"@web/core/dropdown/dropdown_group\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { registry } from \"@web/core/registry\";\n\nimport { Component, useChildSubEnv, useRef, useState } from \"@odoo/owl\";\nimport { useCommand } from \"@web/core/commands/command_hook\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { symmetricalDifference } from \"@web/core/utils/arrays\";\nimport { useChildRef, useService } from \"@web/core/utils/hooks\";\nimport { SwitchCompanyItem } from \"@web/webclient/switch_company_menu/switch_company_item\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\n\nexport class CompanySelector {\n    constructor(companyService, dropdownState) {\n        this.companyService = companyService;\n        this.dropdownState = dropdownState;\n        this.selectedCompaniesIds = companyService.activeCompanyIds.slice();\n    }\n\n    get hasSelectionChanged() {\n        return (\n            symmetricalDifference(this.selectedCompaniesIds, this.companyService.activeCompanyIds)\n                .length > 0\n        );\n    }\n\n    isCompanySelected(companyId) {\n        return this.selectedCompaniesIds.includes(companyId);\n    }\n\n    switchCompany(mode, companyId) {\n        if (mode === \"toggle\") {\n            if (this.selectedCompaniesIds.includes(companyId)) {\n                this._deselectCompany(companyId);\n            } else {\n                this._selectCompany(companyId);\n            }\n        } else if (mode === \"loginto\") {\n            if (this._isSingleCompanyMode()) {\n                this.selectedCompaniesIds.splice(0, this.selectedCompaniesIds.length);\n            }\n            this._selectCompany(companyId, true);\n            this.apply();\n\n            this.dropdownState.close?.();\n        }\n    }\n\n    apply() {\n        this.companyService.setCompanies(this.selectedCompaniesIds, false);\n    }\n\n    reset() {\n        this.selectedCompaniesIds = this.companyService.activeCompanyIds.slice();\n    }\n\n    selectAll() {\n        if (this.selectedCompaniesIds.length > 0) {\n            this.selectedCompaniesIds.splice(0, this.selectedCompaniesIds.length);\n        } else {\n            const newIds = Object.values(this.companyService.allowedCompanies).map((c) => c.id);\n            this.selectedCompaniesIds.splice(0, this.selectedCompaniesIds.length, ...newIds);\n        }\n    }\n\n    _selectCompany(companyId, unshift = false) {\n        if (!this.selectedCompaniesIds.includes(companyId)) {\n            if (unshift) {\n                this.selectedCompaniesIds.unshift(companyId);\n            } else {\n                this.selectedCompaniesIds.push(companyId);\n            }\n        } else if (unshift) {\n            const index = this.selectedCompaniesIds.findIndex((c) => c === companyId);\n            this.selectedCompaniesIds.splice(index, 1);\n            this.selectedCompaniesIds.unshift(companyId);\n        }\n        this._getBranches(companyId).forEach((companyId) => this._selectCompany(companyId));\n    }\n\n    _deselectCompany(companyId) {\n        if (this.selectedCompaniesIds.includes(companyId)) {\n            this.selectedCompaniesIds.splice(this.selectedCompaniesIds.indexOf(companyId), 1);\n            this._getBranches(companyId).forEach((companyId) => this._deselectCompany(companyId));\n        }\n    }\n\n    _getBranches(companyId) {\n        return this.companyService.getCompany(companyId).child_ids || [];\n    }\n\n    _isSingleCompanyMode() {\n        if (this.selectedCompaniesIds.length === 1) {\n            return true;\n        }\n\n        const getActiveCompany = (companyId) => {\n            const isActive = this.selectedCompaniesIds.includes(companyId);\n            return isActive ? this.companyService.getCompany(companyId) : null;\n        };\n\n        let rootCompany = undefined;\n        for (const companyId of this.selectedCompaniesIds) {\n            let company = getActiveCompany(companyId);\n\n            // Find the root active parent of the company\n            while (getActiveCompany(company.parent_id)) {\n                company = getActiveCompany(company.parent_id);\n            }\n\n            if (rootCompany === undefined) {\n                rootCompany = company;\n            } else if (rootCompany !== company) {\n                return false;\n            }\n        }\n\n        // If some children or sub-children of the root company\n        // are not active, we are in multi-company mode.\n        if (rootCompany && rootCompany.child_ids) {\n            const queue = [...rootCompany.child_ids];\n            while (queue.length > 0) {\n                const company = getActiveCompany(queue.pop());\n                if (company && company.child_ids) {\n                    queue.push(...company.child_ids);\n                } else if (!company) {\n                    return false;\n                }\n            }\n        }\n\n        return true;\n    }\n}\n\nexport class SwitchCompanyMenu extends Component {\n    static template = \"web.SwitchCompanyMenu\";\n    static components = { Dropdown, DropdownItem, DropdownGroup, SwitchCompanyItem };\n    static props = {};\n    static CompanySelector = CompanySelector;\n\n    setup() {\n        this.dropdown = useDropdownState();\n        this.companyService = useService(\"company\");\n\n        this.companySelector = useState(\n            new this.constructor.CompanySelector(this.companyService, this.dropdown)\n        );\n        useChildSubEnv({ companySelector: this.companySelector });\n\n        this.searchInputRef = useRef(\"inputRef\");\n        this.state = useState({});\n        this.resetState();\n\n        useHotkey(\"control+enter\", () => this.confirm(), {\n            bypassEditableProtection: true,\n            isAvailable: () => this.companySelector.hasSelectionChanged,\n        });\n\n        useCommand(_t(\"Switch Company\"), () => this.dropdown.open(), { hotkey: \"alt+shift+u\" });\n\n        this.containerRef = useChildRef();\n        this.navigationOptions = {\n            hotkeys: {\n                space: (index, items) => {\n                    if (!items[index]) {\n                        return;\n                    }\n                    if (items[index].el.classList.contains(\"o_switch_company_item\")) {\n                        const companyId = parseInt(items[index].el.dataset.companyId);\n                        this.companySelector.switchCompany(\"toggle\", companyId);\n                    }\n                },\n                enter: (index, items) => {\n                    if (!items[index]) {\n                        return;\n                    }\n                    if (items[index].el.classList.contains(\"o_switch_company_item\")) {\n                        const companyId = parseInt(items[index].el.dataset.companyId);\n                        this.companySelector.switchCompany(\"loginto\", companyId);\n                        this.dropdown.close();\n                    } else {\n                        items[index].select();\n                    }\n                },\n            },\n        };\n    }\n\n    get hasLotsOfCompanies() {\n        return Object.values(this.companyService.allowedCompaniesWithAncestors).length > 9;\n    }\n\n    get companiesEntries() {\n        const companies = [];\n\n        const addCompany = (company, level = 0) => {\n            if (this.matchSearch(company.name)) {\n                companies.push({ company, level });\n            }\n\n            if (company.child_ids) {\n                for (const companyId of company.child_ids) {\n                    addCompany(this.companyService.getCompany(companyId), level + 1);\n                }\n            }\n        };\n\n        Object.values(this.companyService.allowedCompaniesWithAncestors)\n            .filter((c) => !c.parent_id)\n            .sort((c1, c2) => c1.sequence - c2.sequence)\n            .forEach((c) => addCompany(c));\n\n        return companies;\n    }\n\n    get selectAllClass() {\n        if (\n            this.companySelector.selectedCompaniesIds.length >=\n            Object.values(this.companyService.allowedCompanies).length\n        ) {\n            return \"btn-link text-primary\";\n        } else {\n            return \"btn-link text-secondary\";\n        }\n    }\n\n    get selectAllIcon() {\n        if (\n            this.companySelector.selectedCompaniesIds.length >=\n            Object.values(this.companyService.allowedCompanies).length\n        ) {\n            return \"fa-check-square text-primary\";\n        } else if (this.companySelector.selectedCompaniesIds.length > 0) {\n            return \"fa-minus-square-o\";\n        } else {\n            return \"fa-square-o\";\n        }\n    }\n\n    resetState() {\n        this.state.searchFilter = \"\";\n        this.state.showFilter = this.hasLotsOfCompanies;\n    }\n\n    onSearch(ev) {\n        this.state.searchFilter = ev.target.value;\n        this.state.showFilter = true;\n    }\n\n    matchSearch(companyName) {\n        if (!this.state.searchFilter) {\n            return true;\n        }\n\n        const name = companyName.toLocaleLowerCase().replace(/\\s/g, \"\");\n        const filter = this.state.searchFilter.toLocaleLowerCase().replace(/\\s/g, \"\");\n        return name.includes(filter);\n    }\n\n    handleDropdownChange(isOpen) {\n        if (isOpen) {\n            if (this.searchInputRef.el) {\n                this.searchInputRef.el.focus();\n            }\n\n            if (this.containerRef.el) {\n                // Fixes the container width so it doesn't change when searching.\n                const currentWidth = this.containerRef.el.getBoundingClientRect().width;\n                this.containerRef.el.style.width = currentWidth + \"px\";\n            }\n        } else {\n            this.resetState();\n        }\n    }\n\n    confirm() {\n        this.dropdown.close();\n        this.companySelector.apply();\n    }\n\n    get isSingleCompany() {\n        return Object.values(this.companyService.allowedCompaniesWithAncestors ?? {}).length === 1;\n    }\n}\n\nexport const systrayItem = {\n    Component: SwitchCompanyMenu,\n};\n\nregistry.category(\"systray\").add(\"SwitchCompanyMenu\", systrayItem, { sequence: 1 });\n", "import { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownGroup } from \"@web/core/dropdown/dropdown_group\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { session } from \"@web/session\";\n\nimport { Component } from \"@odoo/owl\";\nimport { imageUrl } from \"@web/core/utils/urls\";\n\nconst userMenuRegistry = registry.category(\"user_menuitems\");\n\nexport class UserMenu extends Component {\n    static template = \"web.UserMenu\";\n    static components = { DropdownGroup, Dropdown, DropdownItem, CheckBox };\n    static props = {};\n\n    setup() {\n        this.userName = user.name;\n        this.dbName = session.db;\n        const { partnerId, writeDate } = user;\n        this.source = imageUrl(\"res.partner\", partnerId, \"avatar_128\", { unique: writeDate });\n    }\n\n    getElements() {\n        const sortedItems = userMenuRegistry\n            .getAll()\n            .map((element) => element(this.env))\n            .filter((element) => (element.show ? element.show() : true))\n            .sort((x, y) => {\n                const xSeq = x.sequence ? x.sequence : 100;\n                const ySeq = y.sequence ? y.sequence : 100;\n                return xSeq - ySeq;\n            });\n        return sortedItems;\n    }\n}\n\nexport const systrayItem = {\n    Component: UserMenu,\n};\nregistry.category(\"systray\").add(\"web.user_menu\", systrayItem, { sequence: 0 });\n", "import { Component, markup } from \"@odoo/owl\";\nimport { isMacOS } from \"@web/core/browser/feature_detection\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { user } from \"@web/core/user\";\nimport { escape } from \"@web/core/utils/strings\";\nimport { session } from \"@web/session\";\nimport { browser } from \"../../core/browser/browser\";\nimport { registry } from \"../../core/registry\";\n\nfunction documentationItem(env) {\n    const documentationURL = \"https://www.odoo.com/documentation/18.0\";\n    return {\n        type: \"item\",\n        id: \"documentation\",\n        description: _t(\"Documentation\"),\n        href: documentationURL,\n        callback: () => {\n            browser.open(documentationURL, \"_blank\");\n        },\n        sequence: 10,\n    };\n}\n\nfunction supportItem(env) {\n    const url = session.support_url;\n    return {\n        type: \"item\",\n        id: \"support\",\n        description: _t(\"Support\"),\n        href: url,\n        callback: () => {\n            browser.open(url, \"_blank\");\n        },\n        sequence: 20,\n    };\n}\n\nclass ShortcutsFooterComponent extends Component {\n    static template = \"web.UserMenu.ShortcutsFooterComponent\";\n    static props = {\n        switchNamespace: { type: Function, optional: true },\n    };\n    setup() {\n        this.runShortcutKey = isMacOS() ? \"CONTROL\" : \"ALT\";\n    }\n}\n\nfunction shortCutsItem(env) {\n    // \u2139\ufe0f `_t` can only be inlined directly inside JS template literals after\n    // Babel has been updated to version 2.12.\n    const translatedText = _t(\"Shortcuts\");\n    return {\n        type: \"item\",\n        id: \"shortcuts\",\n        hide: env.isSmall,\n        description: markup(\n            `<div class=\"d-flex align-items-center justify-content-between\">\n                <span>${escape(translatedText)}</span>\n                <span class=\"fw-bold\">${isMacOS() ? \"CMD\" : \"CTRL\"}+K</span>\n            </div>`\n        ),\n        callback: () => {\n            env.services.command.openMainPalette({ FooterComponent: ShortcutsFooterComponent });\n        },\n        sequence: 30,\n    };\n}\n\nfunction separator() {\n    return {\n        type: \"separator\",\n        sequence: 40,\n    };\n}\n\nexport function preferencesItem(env) {\n    return {\n        type: \"item\",\n        id: \"settings\",\n        description: _t(\"Preferences\"),\n        callback: async function () {\n            const actionDescription = await env.services.orm.call(\"res.users\", \"action_get\");\n            actionDescription.res_id = user.userId;\n            env.services.action.doAction(actionDescription);\n        },\n        sequence: 50,\n    };\n}\n\nexport function odooAccountItem(env) {\n    return {\n        type: \"item\",\n        id: \"account\",\n        description: _t(\"My Odoo.com account\"),\n        callback: () => {\n            rpc(\"/web/session/account\")\n                .then((url) => {\n                    browser.open(url, \"_blank\");\n                })\n                .catch(() => {\n                    browser.open(\"https://accounts.odoo.com/account\", \"_blank\");\n                });\n        },\n        sequence: 60,\n    };\n}\n\nfunction installPWAItem(env) {\n    let description = _t(\"Install App\");\n    let callback = () => env.services.pwa.show();\n    let show = () => env.services.pwa.isAvailable;\n    const currentApp = env.services.menu.getCurrentApp();\n    if (currentApp && [\"barcode\", \"field-service\", \"shop-floor\"].includes(currentApp.actionPath)) {\n        // While the feature could work with all apps, we have decided to only\n        // support the installation of the apps contained in this list\n        // The list can grow in the future, by simply adding their path\n        description = _t(\"Install %s\", currentApp.name);\n        callback = () => {\n            window.open(\n                `/scoped_app?app_id=${currentApp.webIcon.split(\",\")[0]}&path=${encodeURIComponent(\n                    \"scoped_app/\" + currentApp.actionPath\n                )}`\n            );\n        };\n        show = () => !env.services.pwa.isScopedApp;\n    }\n    return {\n        type: \"item\",\n        id: \"install_pwa\",\n        description,\n        callback,\n        show,\n        sequence: 65,\n    };\n}\n\nfunction logOutItem(env) {\n    let route = \"/web/session/logout\";\n    if (env.services.pwa.isScopedApp) {\n        route += `?redirect=${encodeURIComponent(env.services.pwa.startUrl)}`;\n    }\n    return {\n        type: \"item\",\n        id: \"logout\",\n        description: _t(\"Log out\"),\n        href: `${browser.location.origin}${route}`,\n        callback: () => {\n            browser.location.href = route;\n        },\n        sequence: 70,\n    };\n}\n\nregistry\n    .category(\"user_menuitems\")\n    .add(\"documentation\", documentationItem)\n    .add(\"support\", supportItem)\n    .add(\"shortcuts\", shortCutsItem)\n    .add(\"separator\", separator)\n    .add(\"profile\", preferencesItem)\n    .add(\"odoo_account\", odooAccountItem)\n    .add(\"install_pwa\", installPWAItem)\n    .add(\"log_out\", logOutItem);\n", "import { useOwnDebugContext } from \"@web/core/debug/debug_context\";\nimport { DebugMenu } from \"@web/core/debug/debug_menu\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { MainComponentsContainer } from \"@web/core/main_components_container\";\nimport { registry } from \"@web/core/registry\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { ActionContainer } from \"./actions/action_container\";\nimport { NavBar } from \"./navbar/navbar\";\n\nimport { Component, onMounted, onWillStart, useExternalListener, useState } from \"@odoo/owl\";\nimport { router, routerBus } from \"@web/core/browser/router\";\nimport { browser } from \"@web/core/browser/browser\";\n\nexport class WebClient extends Component {\n    static template = \"web.WebClient\";\n    static props = {};\n    static components = {\n        ActionContainer,\n        NavBar,\n        MainComponentsContainer,\n    };\n\n    setup() {\n        this.menuService = useService(\"menu\");\n        this.actionService = useService(\"action\");\n        this.title = useService(\"title\");\n        useOwnDebugContext({ categories: [\"default\"] });\n        if (this.env.debug) {\n            registry.category(\"systray\").add(\n                \"web.debug_mode_menu\",\n                {\n                    Component: DebugMenu,\n                },\n                { sequence: 100 }\n            );\n        }\n        this.localization = localization;\n        this.state = useState({\n            fullscreen: false,\n        });\n        useBus(routerBus, \"ROUTE_CHANGE\", this.loadRouterState);\n        useBus(this.env.bus, \"ACTION_MANAGER:UI-UPDATED\", ({ detail: mode }) => {\n            if (mode !== \"new\") {\n                this.state.fullscreen = mode === \"fullscreen\";\n            }\n        });\n        useBus(this.env.bus, \"WEBCLIENT:LOAD_DEFAULT_APP\", this._loadDefaultApp);\n        onMounted(() => {\n            this.loadRouterState();\n            // the chat window and dialog services listen to 'web_client_ready' event in\n            // order to initialize themselves:\n            this.env.bus.trigger(\"WEB_CLIENT_READY\");\n        });\n        useExternalListener(window, \"click\", this.onGlobalClick, { capture: true });\n        onWillStart(this.registerServiceWorker);\n    }\n\n    async loadRouterState() {\n        // ** url-retrocompatibility **\n        // the menu_id in the url is only possible if we came from an old url\n        let menuId = Number(router.current.menu_id || 0);\n        const firstAction = router.current.actionStack?.[0]?.action;\n        if (!menuId && firstAction) {\n            menuId = this.menuService\n                .getAll()\n                .find((m) => m.actionID === firstAction || m.actionPath === firstAction)?.appID;\n        }\n        if (menuId) {\n            this.menuService.setCurrentMenu(menuId);\n        }\n        let stateLoaded = await this.actionService.loadState();\n\n        // ** url-retrocompatibility **\n        // when there is only menu_id in url\n        if (!stateLoaded && menuId) {\n            // Determines the current actionId based on the current menu\n            const menu = this.menuService.getAll().find((m) => menuId === m.id);\n            const actionId = menu && menu.actionID;\n            if (actionId) {\n                await this.actionService.doAction(actionId, { clearBreadcrumbs: true });\n                stateLoaded = true;\n            }\n        }\n\n        // Setting the menu based on the action after it was loaded (eg when the action in url is an xmlid)\n        if (stateLoaded && !menuId) {\n            // Determines the current menu based on the current action\n            const currentController = this.actionService.currentController;\n            const actionId = currentController && currentController.action.id;\n            menuId = this.menuService.getAll().find((m) => m.actionID === actionId)?.appID;\n            if (!menuId) {\n                // Setting the menu based on the session storage if no other menu was found\n                menuId = Number(browser.sessionStorage.getItem(\"menu_id\"));\n            }\n            if (menuId) {\n                // Sets the menu according to the current action\n                this.menuService.setCurrentMenu(menuId);\n            }\n        }\n\n        // Scroll to anchor after the state is loaded\n        if (stateLoaded) {\n            if (browser.location.hash !== \"\") {\n                try {\n                    const el = document.querySelector(browser.location.hash);\n                    if (el !== null) {\n                        el.scrollIntoView(true);\n                    }\n                } catch {\n                    // do nothing if the hash is not a correct selector.\n                }\n            }\n        }\n\n        if (!stateLoaded) {\n            // If no action => falls back to the default app\n            await this._loadDefaultApp();\n        }\n    }\n\n    _loadDefaultApp() {\n        // Selects the first root menu if any\n        const root = this.menuService.getMenu(\"root\");\n        const firstApp = root.children[0];\n        if (firstApp) {\n            return this.menuService.selectMenu(firstApp);\n        }\n    }\n\n    /**\n     * @param {MouseEvent} ev\n     */\n    onGlobalClick(ev) {\n        // When a ctrl-click occurs inside an <a href/> element\n        // we let the browser do the default behavior and\n        // we do not want any other listener to execute.\n        if (\n            (ev.ctrlKey || ev.metaKey) &&\n            !ev.target.isContentEditable &&\n            ((ev.target instanceof HTMLAnchorElement && ev.target.href) ||\n                (ev.target instanceof HTMLElement && ev.target.closest(\"a[href]:not([href=''])\")))\n        ) {\n            ev.stopImmediatePropagation();\n            return;\n        }\n    }\n\n    registerServiceWorker() {\n        if (navigator.serviceWorker) {\n            navigator.serviceWorker\n                .register(\"/web/service-worker.js\", { scope: \"/odoo\" })\n                .catch((error) => {\n                    console.error(\"Service worker registration failed, error:\", error);\n                });\n        }\n    }\n}\n", "import { url } from '@web/core/utils/urls';\nimport { useService } from '@web/core/utils/hooks';\n\nimport { Component, onWillUnmount } from '@odoo/owl';\n\nexport class AppsBar extends Component {\n\tstatic template = 'muk_web_appsbar.AppsBar';\n    static props = {};\n\tsetup() {\n\t\tthis.companyService = useService('company');\n        this.appMenuService = useService('app_menu');\n    \tif (this.companyService.currentCompany.has_appsbar_image) {\n            this.sidebarImageUrl = url('/web/image', {\n                model: 'res.company',\n                field: 'appbar_image',\n                id: this.companyService.currentCompany.id,\n            });\n    \t}\n    \tconst renderAfterMenuChange = () => {\n            this.render();\n        };\n        this.env.bus.addEventListener(\n        \t'MENUS:APP-CHANGED', renderAfterMenuChange\n        );\n        onWillUnmount(() => {\n            this.env.bus.removeEventListener(\n            \t'MENUS:APP-CHANGED', renderAfterMenuChange\n            );\n        });\n    }\n    _onAppClick(app) {\n        return this.appMenuService.selectApp(app);\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\n\nimport { computeAppsAndMenuItems, reorderApps } from \"@web/webclient/menus/menu_helpers\";\n\nexport const appMenuService = {\n    dependencies: [\"menu\"],\n    async start(env, { menu }) {\n        return {\n        \tgetCurrentApp () {\n        \t\treturn menu.getCurrentApp();\n        \t},\n        \tgetAppsMenuItems() {\n\t\t\t\tconst menuItems = computeAppsAndMenuItems(\n\t\t\t\t\tmenu.getMenuAsTree('root')\n\t\t\t\t)\n\t\t\t\tconst apps = menuItems.apps;\n\t\t\t\tconst menuConfig = JSON.parse(\n\t\t\t\t\tuser.settings?.homemenu_config || 'null'\n\t\t\t\t);\n\t\t\t\tif (menuConfig) {\n                    reorderApps(apps, menuConfig);\n\t\t\t\t}\n        \t\treturn apps;\n\t\t\t},\n\t\t\tselectApp(app) {\n\t\t\t\tmenu.selectMenu(app);\n\t\t\t}\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"app_menu\", appMenuService);\n", "import { patch } from '@web/core/utils/patch';\n\nimport { WebClient } from '@web/webclient/webclient';\nimport { AppsBar } from '@muk_web_appsbar/webclient/appsbar/appsbar';\n\npatch(WebClient, {\n    components: {\n        ...WebClient.components,\n        AppsBar,\n    },\n});\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { useSetupAction } from \"@web/search/action_hook\";\nimport { Layout } from \"@web/search/layout\";\nimport { getDefaultConfig } from \"@web/views/view\";\nimport { useEnrichWithActionLinks } from \"@web/webclient/actions/reports/report_hook\";\n\nimport { Component, useRef, useSubEnv } from \"@odoo/owl\";\n\n/**\n * Most of the time reports are printed as pdfs.\n * However, reports have 3 possible actions: pdf, text and HTML.\n * This file is the HTML action.\n * The HTML action is a client action (with control panel) rendering the template in an iframe.\n * If not defined as the default action, the HTML is the fallback to pdf if wkhtmltopdf is not available.\n *\n * It has a button to print the report.\n * It uses a feature to automatically create links to other odoo pages if the selector [res-id][res-model][view-type]\n * is detected.\n */\nexport class ReportAction extends Component {\n    static components = { Layout };\n    static template = \"web.ReportAction\";\n    static props = [\"*\"];\n    setup() {\n        useSubEnv({\n            config: {\n                ...getDefaultConfig(),\n                ...this.env.config,\n            },\n        });\n        useSetupAction();\n\n        this.action = useService(\"action\");\n        this.title = this.props.display_name || this.props.name;\n        this.reportUrl = this.props.report_url;\n        this.iframe = useRef(\"iframe\");\n        useEnrichWithActionLinks(this.iframe);\n    }\n\n    onIframeLoaded(ev) {\n        const iframeDocument = ev.target.contentWindow.document;\n        iframeDocument.body.classList.add(\"o_in_iframe\", \"container-fluid\");\n        iframeDocument.body.classList.remove(\"container\");\n    }\n\n    print() {\n        this.action.doAction({\n            type: \"ir.actions.report\",\n            report_type: \"qweb-pdf\",\n            report_name: this.props.report_name,\n            report_file: this.props.report_file,\n            data: this.props.data || {},\n            context: this.props.context || {},\n            display_name: this.title,\n        });\n    }\n}\n", "import { useComponent, useEffect } from \"@odoo/owl\";\n\n/**\n * Hook used to enrich html and provide automatic links to action.\n * Dom elements must have those attrs [res-id][res-model][view-type]\n * Each element with those attrs will become a link to the specified resource.\n * Works with Iframes.\n *\n * @param {owl reference} ref Owl ref to the element to enrich\n * @param {string} [selector] Selector to apply to the element resolved by the ref.\n */\nexport function useEnrichWithActionLinks(ref, selector = null) {\n    const comp = useComponent();\n    useEffect(\n        (element) => {\n            // If we get an iframe, we need to wait until everything is loaded\n            if (element.matches(\"iframe\")) {\n                element.onload = () => enrich(comp, element, selector, true);\n            } else {\n                enrich(comp, element, selector);\n            }\n        },\n        () => [ref.el]\n    );\n}\n\nfunction enrich(component, targetElement, selector, isIFrame = false) {\n    let doc = window.document;\n\n    // If we are in an iframe, we need to take the right document\n    // both for the element and the doc\n    if (isIFrame) {\n        targetElement = targetElement.contentDocument;\n        doc = targetElement;\n    }\n\n    // If there are selector, we may have multiple blocks of code to enrich\n    const targets = [];\n    if (selector) {\n        targets.push(...targetElement.querySelectorAll(selector));\n    } else {\n        targets.push(targetElement);\n    }\n\n    // Search the elements with the selector, update them and bind an action.\n    for (const currentTarget of targets) {\n        const elementsToWrap = currentTarget.querySelectorAll(\"[res-id][res-model][view-type]\");\n        for (const element of elementsToWrap.values()) {\n            const wrapper = doc.createElement(\"a\");\n            wrapper.setAttribute(\"href\", \"#\");\n            wrapper.addEventListener(\"click\", (ev) => {\n                ev.preventDefault();\n                component.env.services.action.doAction({\n                    type: \"ir.actions.act_window\",\n                    view_mode: element.getAttribute(\"view-type\"),\n                    res_id: Number(element.getAttribute(\"res-id\")),\n                    res_model: element.getAttribute(\"res-model\"),\n                    views: [[element.getAttribute(\"view-id\"), element.getAttribute(\"view-type\")]],\n                });\n            });\n            element.parentNode.insertBefore(wrapper, element);\n            wrapper.appendChild(element);\n        }\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { download } from \"@web/core/network/download\";\n\n/**\n * Generates the report url given a report action.\n *\n * @param {Object} action the report action\n * @param {\"text\"|\"qweb\"|\"html\"} type the type of the report\n * @param {Object} userContext the user context\n * @returns {string}\n */\nexport function getReportUrl(action, type, userContext) {\n    let url = `/report/${type}/${action.report_name}`;\n    const actionContext = action.context || {};\n    if (action.data && JSON.stringify(action.data) !== \"{}\") {\n        // build a query string with `action.data` (it's the place where reports\n        // using a wizard to customize the output traditionally put their options)\n        const options = encodeURIComponent(JSON.stringify(action.data));\n        const context = encodeURIComponent(JSON.stringify(actionContext));\n        url += `?options=${options}&context=${context}`;\n    } else {\n        if (actionContext.active_ids) {\n            url += `/${actionContext.active_ids.join(\",\")}`;\n        }\n        if (type === \"html\") {\n            const context = encodeURIComponent(JSON.stringify(userContext));\n            url += `?context=${context}`;\n        }\n    }\n    return url;\n}\n\n// messages that might be shown to the user dependening on the state of wkhtmltopdf\nfunction getWKHTMLTOPDF_MESSAGES(status) {\n    const link = '<br><br><a href=\"http://wkhtmltopdf.org/\" target=\"_blank\">wkhtmltopdf.org</a>'; // FIXME missing markup\n    const _status = {\n        broken: _t(\n            \"Your installation of Wkhtmltopdf seems to be broken. The report will be shown in html.%(link)s\",\n            { link }\n        ),\n        install: _t(\n            \"Unable to find Wkhtmltopdf on this system. The report will be shown in html.%(link)s\",\n            { link }\n        ),\n        upgrade: _t(\n            \"You should upgrade your version of Wkhtmltopdf to at least 0.12.0 in order to get a correct display of headers and footers as well as support for table-breaking between pages.%(link)s\",\n            { link }\n        ),\n        workers: _t(\n            \"You need to start Odoo with at least two workers to print a pdf version of the reports.\"\n        ),\n    };\n    return _status[status];\n}\n\n/**\n * Launches download action of the report\n *\n * @param {Function} rpc a function to perform RPCs\n * @param {Object} action the report action\n * @param {\"pdf\"|\"text\"} type the type of the report to download\n * @param {Object} userContext the user context\n * @returns {Promise<{success: boolean, message?: string}>}\n */\nexport async function downloadReport(rpc, action, type, userContext) {\n    let message;\n    if (type === \"pdf\") {\n        // Cache the wkhtml status on the function. In prod this means is only\n        // checked once, but we can reset it between tests to test multiple statuses.\n        downloadReport.wkhtmltopdfStatusProm ||= rpc(\"/report/check_wkhtmltopdf\");\n        const status = await downloadReport.wkhtmltopdfStatusProm;\n        message = getWKHTMLTOPDF_MESSAGES(status);\n        if (![\"upgrade\", \"ok\"].includes(status)) {\n            return { success: false, message };\n        }\n    }\n    const url = getReportUrl(action, type);\n    await download({\n        url: \"/report/download\",\n        data: {\n            data: JSON.stringify([url, action.report_type]),\n            context: JSON.stringify(userContext),\n        },\n    });\n    return { success: true, message };\n}\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", "/** @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        return { updateBusPresence };\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 { deserializeDateTime } from \"@web/core/l10n/dates\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\n\nconst { DateTime } = luxon;\nexport class OutdatedPageWatcherService {\n    constructor(env, services) {\n        this.setup(env, services);\n    }\n\n    /**\n     * @param {import(\"@web/env\").OdooEnv}\n     * @param {Partial<import(\"services\").Services>} services\n     */\n    setup(env, { bus_service, multi_tab, notification }) {\n        this.notification = notification;\n        const vacuumInfo = multi_tab.getSharedValue(\"bus.autovacuum_info\");\n        this.lastAutovacuumDt = vacuumInfo ? deserializeDateTime(vacuumInfo.lastcall) : null;\n        this.nextAutovacuumDt = vacuumInfo ? deserializeDateTime(vacuumInfo.nextcall) : null;\n        this.lastDisconnectDt = null;\n        this.closeNotificationFn;\n        bus_service.addEventListener(\"disconnect\", () => (this.lastDisconnectDt = DateTime.now()));\n        bus_service.addEventListener(\"reconnect\", async () => {\n            if (!multi_tab.isOnMainTab() || !this.lastDisconnectDt) {\n                return;\n            }\n            if (!this.lastAutovacuumDt || DateTime.now() >= this.nextAutovacuumDt) {\n                const { lastcall, nextcall } = await rpc(\n                    \"/bus/get_autovacuum_info\",\n                    {},\n                    { silent: true }\n                );\n                this.lastAutovacuumDt = deserializeDateTime(lastcall);\n                this.nextAutovacuumDt = deserializeDateTime(nextcall);\n                multi_tab.setSharedValue(\"bus.autovacuum_info\", { lastcall, nextcall });\n            }\n            if (this.lastDisconnectDt <= this.lastAutovacuumDt) {\n                this.showOutdatedPageNotification();\n            }\n        });\n        multi_tab.bus.addEventListener(\"shared_value_updated\", ({ detail: { key, newValue } }) => {\n            if (key !== \"bus.autovacuum_info\") {\n                return;\n            }\n            const infos = JSON.parse(newValue);\n            this.lastAutovacuumDt = deserializeDateTime(infos.lastcall);\n            this.nextAutovacuumDt = deserializeDateTime(infos.nextcall);\n            if (this.lastDisconnectDt <= this.lastAutovacuumDt) {\n                this.showOutdatedPageNotification();\n            }\n        });\n    }\n\n    showOutdatedPageNotification() {\n        this.closeNotificationFn?.();\n        this.closeNotificationFn = this.notification.add(\n            _t(\"Save your work and refresh to get the latest updates and avoid potential issues.\"),\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: () => browser.location.reload(),\n                    },\n                ],\n            }\n        );\n    }\n}\n\nexport const outdatedPageWatcherService = {\n    dependencies: [\"bus_service\", \"multi_tab\", \"notification\"],\n    start(env, services) {\n        return new OutdatedPageWatcherService(env, services);\n    },\n};\n\nregistry.category(\"services\").add(\"bus.outdated_page_watcher\", outdatedPageWatcherService);\n", "/* @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\n\nexport const simpleNotificationService = {\n    dependencies: [\"bus_service\", \"notification\"],\n    start(env, { bus_service, notification: notificationService }) {\n        bus_service.subscribe(\"simple_notification\", ({ message, sticky, title, type }) => {\n            notificationService.add(message, { sticky, title, type });\n        });\n        bus_service.start();\n    },\n};\n\nregistry.category(\"services\").add(\"simple_notification\", simpleNotificationService);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"@web/core/registry\";\nimport { session } from \"@web/session\";\n\nexport const assetsWatchdogService = {\n    dependencies: [\"bus_service\", \"notification\"],\n\n    start(env, { bus_service, notification }) {\n        let isNotificationDisplayed = false;\n        let bundleNotifTimerID = null;\n\n        bus_service.subscribe(\"bundle_changed\", ({ server_version }) => {\n            if (server_version !== session.server_version) {\n                displayBundleChangedNotification();\n            }\n        });\n        bus_service.start();\n\n        /**\n         * Displays one notification on user's screen when assets have changed\n         */\n        function displayBundleChangedNotification() {\n            if (!isNotificationDisplayed) {\n                // Wrap the notification inside a delay.\n                // The server may be overwhelmed with recomputing assets\n                // We wait until things settle down\n                browser.clearTimeout(bundleNotifTimerID);\n                bundleNotifTimerID = browser.setTimeout(() => {\n                    notification.add(_t(\"The page appears to be out of date.\"), {\n                        title: _t(\"Refresh\"),\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                        onClose: () => {\n                            isNotificationDisplayed = false;\n                        },\n                    });\n                    isNotificationDisplayed = true;\n                }, getBundleNotificationDelay());\n            }\n        }\n\n        /**\n         * Computes a random delay to avoid hammering the server\n         * when bundles change with all the users reloading\n         * at the same time\n         *\n         * @return {number} delay in milliseconds\n         */\n        function getBundleNotificationDelay() {\n            return 10000 + Math.floor(Math.random() * 50) * 1000;\n        }\n    },\n};\n\nregistry.category(\"services\").add(\"assetsWatchdog\", assetsWatchdogService);\n", "import { WORKER_STATE } from \"@bus/workers/websocket_worker\";\nimport { reactive } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"@web/core/registry\";\n\n/**\n * Detect lost connections to the bus. A connection is considered as lost if it\n * couldn't be established after a reconnect attempt.\n */\nexport class BusMonitoringService {\n    isConnectionLost = false;\n\n    constructor(env, services) {\n        const reactiveThis = reactive(this);\n        reactiveThis.setup(env, services);\n        return reactiveThis;\n    }\n\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {Partial<import(\"services\").Services>} services\n     */\n    setup(env, { bus_service }) {\n        bus_service.addEventListener(\"worker_state_updated\", ({ detail }) =>\n            this.workerStateOnChange(detail)\n        );\n        browser.addEventListener(\"offline\", () => (this.isReconnecting = false));\n    }\n\n    /**\n     * Handle state changes for the WebSocket worker.\n     *\n     * @param {WORKER_STATE[keyof WORKER_STATE]} state\n     */\n    workerStateOnChange(state) {\n        if (!navigator.onLine) {\n            return;\n        }\n        switch (state) {\n            case WORKER_STATE.CONNECTING: {\n                this.isReconnecting = true;\n                break;\n            }\n            case WORKER_STATE.CONNECTED: {\n                this.isReconnecting = false;\n                this.isConnectionLost = false;\n                break;\n            }\n            case WORKER_STATE.DISCONNECTED: {\n                if (this.isReconnecting) {\n                    this.isConnectionLost = true;\n                    this.isReconnecting = false;\n                }\n                break;\n            }\n        }\n    }\n}\n\nexport const busMonitoringservice = {\n    dependencies: [\"bus_service\"],\n    start(env, services) {\n        return new BusMonitoringService(env, services);\n    },\n};\n\nregistry.category(\"services\").add(\"bus.monitoring_service\", busMonitoringservice);\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\", \"log_debug\", \"notification\"]);\n// Slightly delay the reconnection when coming back online as the network is not\n// ready yet and the exponential backoff would delay the reconnection by a lot.\nexport const BACK_ONLINE_RECONNECT_DELAY = 5000;\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 *  @emits worker_state_updated\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        let backOnlineTimeout;\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 \"worker_state_updated\":\n                    workerState = data;\n                    break;\n                case \"log_debug\":\n                    console.debug(...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(\n            \"online\",\n            () => {\n                backOnlineTimeout = browser.setTimeout(() => {\n                    if (isActive) {\n                        send(\"start\");\n                    }\n                }, BACK_ONLINE_RECONNECT_DELAY);\n            },\n            { capture: true }\n        );\n        browser.addEventListener(\n            \"offline\",\n            () => {\n                clearTimeout(backOnlineTimeout);\n                send(\"stop\");\n            },\n            {\n                capture: true,\n            }\n        );\n\n        return {\n            addEventListener: bus.addEventListener.bind(bus),\n            addChannel: async (channel) => {\n                if (!worker) {\n                    startWorker();\n                }\n                await connectionInitializedDeferred;\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                }\n                await connectionInitializedDeferred;\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'| 'worker_state_updated' | 'log_debug' } 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        this._logDebug(\"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        this._logDebug(\"sendToClient\", 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        this._logDebug(\"_onClientMessage\", 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 = [...this.debugModeByClient.values()].some(Boolean);\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.set(client, debug);\n            this.isDebug = [...this.debugModeByClient.values()].some(Boolean);\n            this.sendToClient(client, \"update_state\", this.state);\n            this.sendToClient(client, \"initialized\");\n            return;\n        }\n        this.newestStartTs = startTs;\n        this.websocketURL = websocketURL;\n        this.lastNotificationId = lastNotificationId;\n        this.debugModeByClient.set(client, debug);\n        this.isDebug = [...this.debugModeByClient.values()].some(Boolean);\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, \"update_state\", this.state);\n        this.sendToClient(client, \"initialized\");\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._logDebug(\"_onWebsocketClose\", code, reason);\n        this._updateState(WORKER_STATE.DISCONNECTED);\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        this._logDebug(\"_onWebsocketError\");\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        this._logDebug(\"_onWebsocketMessage\", notifications);\n        this.lastNotificationId = notifications[notifications.length - 1].id;\n        this.broadcast(\"notification\", notifications);\n    }\n\n    _logDebug(title, ...args) {\n        const clientsInDebug = [...this.debugModeByClient.keys()].filter((client) =>\n            this.debugModeByClient.get(client)\n        );\n        for (const client of clientsInDebug) {\n            client.postMessage({\n                type: \"log_debug\",\n                data: [\n                    `%c${new Date().toLocaleString()} - [${title}]`,\n                    \"color: #c6e; font-weight: bold;\",\n                    ...args,\n                ],\n            });\n        }\n    }\n\n    /**\n     * Triggered on websocket open. Send message that were waiting for\n     * the connection to open.\n     */\n    _onWebsocketOpen() {\n        this._logDebug(\"_onWebsocketOpen\");\n        this._updateState(WORKER_STATE.CONNECTED);\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._logDebug(\"_retryConnectionWithDelay\", this.connectRetryDelay);\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        this._logDebug(\"_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    _removeWebsocketListeners() {\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\n    /**\n     * Start the worker by opening a websocket connection.\n     */\n    _start() {\n        this._logDebug(\"_start\");\n        if (!this.active || this._isWebsocketConnected() || this._isWebsocketConnecting()) {\n            return;\n        }\n        this._removeWebsocketListeners();\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        this._logDebug(\"_stop\");\n        clearTimeout(this.connectTimeout);\n        this.connectRetryDelay = this.INITIAL_RECONNECT_DELAY;\n        this.isReconnecting = false;\n        this.lastChannelSubscription = null;\n        this.websocket?.close();\n        this._removeWebsocketListeners();\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(\"worker_state_updated\", 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                    let offsetLeft = 0;\n                    let offsetTop = 0;\n                    if (document !== anchor.ownerDocument) {\n                        const iframe = [...document.querySelectorAll(\"iframe\")].filter(\n                            (e) => e.contentDocument === anchor.ownerDocument\n                        )[0];\n                        offsetLeft = iframe.getBoundingClientRect().left;\n                        offsetTop = iframe.getBoundingClientRect().top;\n                    }\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 + offsetLeft + \"px\";\n                    zone.style.top = top + offsetTop + \"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 { Macro } from \"@web/core/macro\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { setupEventActions } from \"@web/../lib/hoot-dom/helpers/events\";\nimport * as hoot from \"@odoo/hoot-dom\";\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.config = tourState.getCurrentConfig() || {};\n    }\n\n    get currentIndex() {\n        return tourState.getCurrentIndex();\n    }\n\n    get currentStep() {\n        return this.steps[this.currentIndex];\n    }\n\n    get debugMode() {\n        return this.config.debug !== false;\n    }\n\n    start() {\n        setupEventActions(document.createElement(\"div\"), { allowSubmit: true });\n        const { delayToCheckUndeterminisms, stepDelay } = this.config;\n        const macroSteps = this.steps\n            .filter((step) => step.index >= this.currentIndex)\n            .flatMap((step) => [\n                {\n                    action: async () => {\n                        if (this.debugMode) {\n                            console.groupCollapsed(step.describeMe);\n                            console.log(step.stringify);\n                            if (step.break) {\n                                // eslint-disable-next-line no-debugger\n                                debugger;\n                            }\n                        } else {\n                            console.log(step.describeMe);\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                        if (stepDelay > 0) {\n                            await hoot.delay(stepDelay);\n                        }\n                    },\n                },\n                {\n                    initialDelay: () => (this.previousStepIsJustACheck ? 0 : null),\n                    trigger: step.trigger ? () => step.findTrigger() : null,\n                    timeout:\n                        step.pause && this.debugMode\n                            ? 9999999\n                            : step.timeout || this.timeout || 10000,\n                    action: async (trigger) => {\n                        if (delayToCheckUndeterminisms > 0) {\n                            await step.checkForUndeterminisms(trigger, delayToCheckUndeterminisms);\n                        }\n                        this.previousStepIsJustACheck = !step.hasAction;\n                        const result = await step.doAction();\n                        if (this.debugMode) {\n                            console.log(trigger);\n                            if (step.skipped) {\n                                console.log(\"This step has been skipped\");\n                            } else {\n                                console.log(\"This step has run successfully\");\n                            }\n                            console.groupEnd();\n                            if (step.pause) {\n                                await this.pause();\n                            }\n                        }\n                        tourState.setCurrentIndex(step.index + 1);\n                        return result;\n                    },\n                },\n            ]);\n\n        const end = () => {\n            delete window.hoot;\n            transitionConfig.disabled = false;\n            tourState.clear();\n            //No need to catch error yet.\n            window.addEventListener(\n                \"error\",\n                (ev) => {\n                    ev.preventDefault();\n                    ev.stopImmediatePropagation();\n                },\n                true\n            );\n            window.addEventListener(\n                \"unhandledrejection\",\n                (ev) => {\n                    ev.preventDefault();\n                    ev.stopImmediatePropagation();\n                },\n                true\n            );\n        };\n\n        this.macro = new Macro({\n            name: this.name,\n            checkDelay: this.checkDelay || 200,\n            steps: macroSteps,\n            onError: (error) => {\n                if (error.type === \"Timeout\") {\n                    this.throwError(...this.currentStep.describeWhyIFailed, error.message);\n                } else {\n                    this.throwError(error.message);\n                }\n                end();\n            },\n            onComplete: () => {\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 ${this.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                end();\n            },\n        });\n        if (this.debugMode && this.currentIndex === 0) {\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        transitionConfig.disabled = true;\n        window.hoot = hoot;\n        this.macro.start();\n    }\n\n    get describeWhereIFailed() {\n        const offset = 3;\n        const start = Math.max(this.currentIndex - offset, 0);\n        const end = Math.min(this.currentIndex + offset, this.steps.length - 1);\n        const result = [];\n        for (let i = start; i <= end; i++) {\n            const step = this.steps[i];\n            const stepString = step.stringify;\n            const text = [stepString];\n            if (i === this.currentIndex) {\n                const line = \"-\".repeat(10);\n                const failing_step = `${line} FAILED: ${step.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     * @param {string} [error]\n     */\n    throwError(...args) {\n        console.groupEnd();\n        tourState.setCurrentTourOnError();\n        // console.error notifies the test runner that the tour failed.\n        browser.console.error([`FAILED: ${this.currentStep.describeMe}.`, ...args].join(\"\\n\"));\n        // The logged text shows the relative position of the failed step.\n        // Useful for finding the failed step.\n        browser.console.dir(this.describeWhereIFailed);\n        if (this.debugMode) {\n            // eslint-disable-next-line no-debugger\n            debugger;\n        }\n    }\n\n    async pause() {\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}\n", "import * as hoot from \"@odoo/hoot-dom\";\nimport { waitForStable } from \"@web/core/macro\";\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        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        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        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        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        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        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        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    async press(...args) {\n        await 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        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        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        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    async uncheck(selector) {\n        const element = this._get_action_element(selector);\n        await 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    async goToUrl(url) {\n        const linkEl = document.createElement(\"a\");\n        linkEl.href = url;\n        //We want DOM is stable before quit it.\n        await waitForStable();\n        await hoot.click(linkEl);\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", "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: element.ownerDocument,\n                conditional: (ev) =>\n                    element.ownerDocument\n                        .elementsFromPoint(ev.clientX, ev.clientY)\n                        .includes(element),\n            });\n            consumeEvents.push({\n                name: \"drop\",\n                target: element.ownerDocument,\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(\n                \".ui-draggable, .o_draggable, .o_we_draggable, .o-draggable, [draggable='true']\"\n            );\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 scrollParentElement =\n                    getScrollParent(this.currentTarget) || document.documentElement;\n                const targetBounds = this.currentTarget.getBoundingClientRect();\n                if (targetBounds.bottom > scrollParentElement.clientHeight) {\n                    this._targetPosition = \"out-below\";\n                } else if (targetBounds.top < 0) {\n                    this._targetPosition = \"out-above\";\n                } else if (targetBounds.left < 0) {\n                    this._targetPosition = \"out-left\";\n                } else if (targetBounds.right > scrollParentElement.clientWidth) {\n                    this._targetPosition = \"out-right\";\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                    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                        floatingAnchor.style.left = `${x + width / 2}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                        floatingAnchor.style.left = `${x + width / 2}px`;\n                    }\n                    if (intersection.targetPosition === \"out-left\") {\n                        tooltipPosition = \"right\";\n                        content = _t(\"Scroll left to reach the next step.\");\n                        floatingAnchor.style.top = `${y + height / 2}px`;\n                        floatingAnchor.style.left = `${x + TourPointer.width}px`;\n                    } else if (intersection.targetPosition === \"out-right\") {\n                        tooltipPosition = \"left\";\n                        content = _t(\"Scroll right to reach the next step.\");\n                        floatingAnchor.style.top = `${y + height / 2}px`;\n                        floatingAnchor.style.left = `${x + width - TourPointer.width}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\";\nimport { tourRecorderState } from \"./tour_recorder_state\";\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        this.state.steps = tourRecorderState.getCurrentTourRecorder();\n        this.state.recording = tourRecorderState.isRecording() === \"1\";\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        tourRecorderState.setCurrentTourRecorder(this.state.steps);\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        tourRecorderState.setCurrentTourRecorder(this.state.steps);\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        tourRecorderState.setCurrentTourRecorder(this.state.steps);\n    }\n\n    toggleRecording() {\n        this.state.recording = !this.state.recording;\n        tourRecorderState.setIsRecording(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        tourRecorderState.clear();\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", "import { browser } from \"@web/core/browser/browser\";\n\nconst CURRENT_TOUR_RECORDER_LOCAL_STORAGE = \"current_tour_recorder\";\nconst CURRENT_TOUR_RECORDER_RECORD_LOCAL_STORAGE = \"current_tour_recorder.record\";\n\n/**\n * Wrapper around localStorage for persistence of the current recording.\n * Useful for resuming recording when the page refreshed.\n */\nexport const tourRecorderState = {\n    isRecording() {\n        return browser.localStorage.getItem(CURRENT_TOUR_RECORDER_RECORD_LOCAL_STORAGE) || \"0\";\n    },\n    setIsRecording(isRecording) {\n        browser.localStorage.setItem(\n            CURRENT_TOUR_RECORDER_RECORD_LOCAL_STORAGE,\n            isRecording ? \"1\" : \"0\"\n        );\n    },\n    setCurrentTourRecorder(tour) {\n        tour = JSON.stringify(tour);\n        browser.localStorage.setItem(CURRENT_TOUR_RECORDER_LOCAL_STORAGE, tour);\n    },\n    getCurrentTourRecorder() {\n        const tour = browser.localStorage.getItem(CURRENT_TOUR_RECORDER_LOCAL_STORAGE) || \"[]\";\n        return JSON.parse(tour);\n    },\n    clear() {\n        browser.localStorage.removeItem(CURRENT_TOUR_RECORDER_LOCAL_STORAGE);\n        browser.localStorage.removeItem(CURRENT_TOUR_RECORDER_RECORD_LOCAL_STORAGE);\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\";\nimport { tourRecorderState } from \"@web_tour/tour_service/tour_recorder/tour_recorder_state\";\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    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 getTourFromRegistry(tourName) {\n            if (!tourRegistry.contains(tourName)) {\n                return;\n            }\n            const tour = tourRegistry.get(tourName);\n            return {\n                ...tour,\n                steps: tour.steps(),\n                name: tourName,\n                wait_for: tour.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 tourFromRegistry = getTourFromRegistry(tourName);\n\n            if (!tourFromRegistry && !options.fromDB) {\n                // Sometime tours are not loaded depending on the modules.\n                // For example, point_of_sale do not load all tours assets.\n                return;\n            }\n\n            const tour = options.fromDB ? { name: tourName, url: options.url } : tourFromRegistry;\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                delayToCheckUndeterminisms: 0,\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\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 = getTourFromRegistry(tourName);\n            if (tourConfig.fromDB) {\n                tour = await getTourFromDB(tourName);\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();\n            } else {\n                new TourInteractive(tour).start(pointer, async () => {\n                    pointer.stop();\n                    tourState.clear();\n                    browser.console.log(\"tour succeeded\");\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                            tourRecorderState.clear();\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                            tourRecorderState.clear();\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 { tourState } from \"./tour_state\";\nimport * as hoot from \"@odoo/hoot-dom\";\nimport { callWithUnloadCheck, serializeChanges, serializeMutation } from \"./tour_utils\";\nimport { TourHelpers } from \"./tour_helpers\";\nimport { TourStep } from \"./tour_step\";\nimport { getTag } from \"@web/core/utils/xml\";\nimport { waitForStable } from \"@web/core/macro\";\n\nexport class TourStepAutomatic extends TourStep {\n    skipped = false;\n    error = \"\";\n    constructor(data, tour, index) {\n        super(data, tour);\n        this.index = index;\n        this.tourConfig = tourState.getCurrentConfig();\n    }\n\n    async checkForUndeterminisms(initialElement, delay) {\n        if (delay <= 0 || !initialElement) {\n            return;\n        }\n        const tagName = initialElement.tagName?.toLowerCase();\n        if ([\"body\", \"html\"].includes(tagName) || !tagName) {\n            return;\n        }\n        const snapshot = initialElement.cloneNode(true);\n        const mutations = await waitForStable(initialElement, delay);\n        let reason;\n        if (!hoot.isVisible(initialElement)) {\n            reason = `Initial element is no longer visible`;\n        } else if (!initialElement.isEqualNode(snapshot)) {\n            reason =\n                `Initial element has changed:\\n` +\n                JSON.stringify(serializeChanges(snapshot, initialElement), null, 2);\n        } else if (mutations.length) {\n            const changes = [...new Set(mutations.map(serializeMutation))];\n            reason =\n                `Initial element has mutated ${mutations.length} times:\\n` +\n                JSON.stringify(changes, null, 2);\n        }\n        if (reason) {\n            throw new Error(\n                `Potential non deterministic behavior found in ${delay}ms for trigger ${this.trigger}.\\n${reason}`\n            );\n        }\n    }\n\n    get describeWhyIFailed() {\n        const errors = [];\n        if (this.element) {\n            errors.push(`Element has been found.`);\n            if (this.isUIBlocked) {\n                errors.push(\"BUT: DOM is blocked by UI.\");\n            }\n            if (!this.elementIsInModal) {\n                errors.push(\n                    `BUT: It is not allowed to do action on an element that's below a modal.`\n                );\n            }\n            if (!this.elementIsEnabled) {\n                errors.push(\n                    `BUT: Element is not enabled. TIP: You can use :enable to wait the element is enabled before doing action on it.`\n                );\n            }\n            if (!this.parentFrameIsReady) {\n                errors.push(`BUT: parent frame is not ready ([is-ready='false']).`);\n            }\n        } else {\n            const checkElement = hoot.queryFirst(this.trigger);\n            if (checkElement) {\n                errors.push(`Element has been found.`);\n                errors.push(\n                    `BUT: Element is not visible. TIP: You can use :not(:visible) to force the search for an invisible element.`\n                );\n            } else {\n                errors.push(`Element (${this.trigger}) has not been found.`);\n            }\n        }\n        return errors;\n    }\n\n    /**\n     * When return null or false, macro continues.\n     */\n    async doAction() {\n        if (this.skipped) {\n            return;\n        }\n        return await callWithUnloadCheck(async () => {\n            const actionHelper = new TourHelpers(this.element);\n            if (typeof this.run === \"function\") {\n                await this.run.call({ anchor: this.element }, actionHelper);\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 actionHelper[m.groups?.action](m.groups?.arguments);\n                }\n            }\n        });\n    }\n\n    /**\n     * Each time it returns false, tour engine wait for a mutation\n     * to retry to find the trigger.\n     * @returns {(HTMLElement|Boolean)}\n     */\n    findTrigger() {\n        if (!this.active) {\n            this.skipped = true;\n            return true;\n        }\n        const visible = !/:(hidden|visible)\\b/.test(this.trigger);\n        this.element = hoot.queryFirst(this.trigger, { visible });\n        if (this.element) {\n            return !this.isUIBlocked &&\n                this.elementIsEnabled &&\n                this.elementIsInModal &&\n                this.parentFrameIsReady\n                ? this.element\n                : false;\n        }\n        return false;\n    }\n\n    get isUIBlocked() {\n        return (\n            document.body.classList.contains(\"o_lazy_js_waiting\") ||\n            document.body.classList.contains(\"o_ui_blocked\") ||\n            document.querySelector(\".o_blockUI\") ||\n            document.querySelector(\".o_is_blocked\")\n        );\n    }\n\n    get parentFrameIsReady() {\n        const parentFrame = hoot.getParentFrame(this.element);\n        return parentFrame && parentFrame.hasAttribute(\"is-ready\")\n            ? parentFrame.getAttribute(\"is-ready\") === \"true\"\n            : true;\n    }\n\n    get elementIsInModal() {\n        if (this.hasAction) {\n            const overlays = hoot.queryFirst(\".popover, .o-we-command, .o_notification\");\n            const modal = hoot.queryFirst(\".modal:visible:not(.o_inactive_modal):last\");\n            if (modal && !overlays && !this.trigger.startsWith(\"body\")) {\n                return (\n                    modal.contains(hoot.getParentFrame(this.element)) ||\n                    modal.contains(this.element)\n                );\n            }\n        }\n        return true;\n    }\n\n    get elementIsEnabled() {\n        const isTag = (array) => array.includes(getTag(this.element, true));\n        if (this.hasAction) {\n            if (isTag([\"input\", \"textarea\"])) {\n                return hoot.isEditable(this.element);\n            } else if (isTag([\"button\", \"select\"])) {\n                return !this.element.disabled;\n            }\n        }\n        return true;\n    }\n\n    get hasAction() {\n        return [\"string\", \"function\"].includes(typeof this.run) && !this.skipped;\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\nfunction formatValue(key, value, maxLength = 200) {\n    if (!value) {\n        return \"(empty)\";\n    }\n    return value.length > maxLength ? value.slice(0, maxLength) + \"...\" : value;\n}\n\nfunction serializeNode(node) {\n    if (node.nodeType === Node.TEXT_NODE) {\n        return `\"${node.nodeValue.trim()}\"`;\n    }\n    return node.outerHTML ? formatValue(\"node\", node.outerHTML, 500) : \"[Unknown Node]\";\n}\n\nexport function serializeChanges(snapshot, current) {\n    const changes = {\n        node: serializeNode(current),\n    };\n    function pushChanges(key, obj) {\n        changes[key] = changes[key] || [];\n        changes[key].push(obj);\n    }\n\n    if (snapshot.textContent !== current.textContent) {\n        pushChanges(\"modifiedText\", { before: snapshot.textContent, after: current.textContent });\n    }\n\n    const oldChildren = [...snapshot.childNodes].filter((node) => node.nodeType !== Node.TEXT_NODE);\n    const newChildren = [...current.childNodes].filter((node) => node.nodeType !== Node.TEXT_NODE);\n    oldChildren.forEach((oldNode, index) => {\n        if (!newChildren[index] || !oldNode.isEqualNode(newChildren[index])) {\n            pushChanges(\"removedNodes\", { oldNode: serializeNode(oldNode) });\n        }\n    });\n    newChildren.forEach((newNode, index) => {\n        if (!oldChildren[index] || !newNode.isEqualNode(oldChildren[index])) {\n            pushChanges(\"addedNodes\", { newNode: serializeNode(newNode) });\n        }\n    });\n\n    const oldAttrNames = new Set([...snapshot.attributes].map((attr) => attr.name));\n    const newAttrNames = new Set([...current.attributes].map((attr) => attr.name));\n    new Set([...oldAttrNames, ...newAttrNames]).forEach((attributeName) => {\n        const oldValue = snapshot.getAttribute(attributeName);\n        const newValue = current.getAttribute(attributeName);\n        const before = oldValue !== newValue || !newAttrNames.has(attributeName) ? oldValue : null;\n        const after = oldValue !== newValue || !oldAttrNames.has(attributeName) ? newValue : null;\n        if (before || after) {\n            pushChanges(\"modifiedAttributes\", { attributeName, before, after });\n        }\n    });\n    return changes;\n}\n\nexport function serializeMutation(mutation) {\n    const { type, attributeName } = mutation;\n    if (type === \"attributes\" && attributeName) {\n        return `attribute: ${attributeName}`;\n    } else {\n        return type;\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                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                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", "import { registry } from \"@web/core/registry\";\nimport { listView } from \"@web/views/list/list_view\";\n\nregistry.category(\"views\").add(\"tour_list\", {\n    ...listView,\n    buttonTemplate: \"web_tour.TourListController.Buttons\",\n});\n", "import { charField, CharField } from \"@web/views/fields/char/char_field\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\n\nexport class TourStartWidget extends CharField {\n    static template = \"web_tour.TourStartWidget\";\n    static props = {\n        ...CharField.props,\n        link: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        this.tour = useService(\"tour_service\");\n    }\n\n    get tourData() {\n        return this.props.record.data;\n    }\n\n    _onStartTour() {\n        this.tour.startTour(this.tourData.name, {\n            mode: \"manual\",\n            url: this.tourData.url,\n            fromDB: this.tourData.custom,\n            rainbowManMessage: this.tourData.rainbow_man_message,\n        });\n    }\n\n    _onTestTour() {\n        this.tour.startTour(this.tourData.name, {\n            mode: \"auto\",\n            url: this.tourData.url,\n            fromDB: this.tourData.custom,\n            stepDelay: 500,\n            showPointerDuration: 250,\n            rainbowManMessage: this.tourData.rainbow_man_message,\n        });\n    }\n}\n\nexport const tourStartWidgetField = {\n    ...charField,\n    component: TourStartWidget,\n    extractProps: ({ options }) => ({\n        link: options.link,\n    }),\n};\n\nregistry.category(\"fields\").add(\"tour_start_widget\", tourStartWidgetField);\n", "/** @odoo-module */\n\nimport { HootDomError, getTag, isFirefox, isIterable, parseRegExp } from \"../hoot_dom_utils\";\nimport { 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 *  inline?: boolean;\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 * @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)$/;\nconst R_LINEBREAK = /\\s*\\n+\\s*/g;\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\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        content = content.replace(R_HORIZONTAL_WHITESPACE, \" \").trim();\n    }\n    if (options?.inline) {\n        content = content.replace(R_LINEBREAK, \" \");\n    }\n    return content;\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 * @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. Further checks are performed\n * in the following cases:\n * - the given node is an iframe (checks in its content document);\n * - the given node has a shadow root (checks in that shadow root document);\n * - the given node is the body of an iframe (checks in the parent document).\n *\n * @param {Node} [node]\n */\nexport function getActiveElement(node) {\n    const document = getDocument(node);\n    const window = getWindow(node);\n    const { activeElement } = document;\n    const { contentDocument, shadowRoot } = activeElement;\n\n    if (contentDocument && contentDocument.activeElement !== contentDocument.body) {\n        // Active element is an \"iframe\" element (with an active element other than its own body):\n        if (contentDocument.activeElement === contentDocument.body) {\n            // Active element is the body of the iframe:\n            // -> returns that element\n            return contentDocument.activeElement;\n        } else {\n            // Active element is something else than the body:\n            // -> get the active element inside the iframe document\n            return getActiveElement(contentDocument);\n        }\n    }\n\n    if (shadowRoot) {\n        // Active element has a shadow root:\n        // -> get the active element inside its root\n        return shadowRoot.activeElement;\n    }\n\n    if (activeElement === document.body && window !== window.parent) {\n        // Active element is the body of an iframe:\n        // -> get the active element of its parent frame (recursively)\n        return getActiveElement(window.parent.document);\n    }\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 = queryOne(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 = queryOne(options?.root || getDefaultRoot());\n    const focusableEls = getFocusableElements({ ...options, parent });\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 document = getDocument(node);\n    if (!document) {\n        return null;\n    }\n    const view = document.defaultView;\n    if (view !== view.parent) {\n        for (const iframe of view.parent.document.getElementsByTagName(\"iframe\")) {\n            if (iframe.contentWindow === view) {\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 = queryOne(options?.root || getDefaultRoot());\n    const focusableEls = getFocusableElements({ ...options, parent });\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 {Promise<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    getParentFrame,\n    getPreviousFocusableElement,\n    getStyle,\n    getWindow,\n    isCheckable,\n    isEditable,\n    isEventTarget,\n    isNode,\n    isNodeFocusable,\n    parseDimensions,\n    parsePosition,\n    queryAll,\n    queryFirst,\n    setDimensions,\n    toSelector,\n} from \"./dom\";\nimport { microTick } from \"./time\";\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 {{\n *  eventInit?: EventInit;\n * }} 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 & QueryOptions & {\n *  force?: boolean;\n *  initiator?: \"keyboard\" | \"scrollbar\" | \"wheel\" | null;\n *  relative?: boolean;\n * }} ScrollOptions\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, create: $create, 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\n//-----------------------------------------------------------------------------\n// Internal\n//-----------------------------------------------------------------------------\n\n/**\n * @param {Event} ev\n */\nconst cancelTrustedEvent = (ev) => {\n    if (ev.isTrusted && runTime.eventsToIgnore.includes(ev.type)) {\n        runTime.eventsToIgnore.splice(runTime.eventsToIgnore.indexOf(ev.type), 1);\n        ev.stopPropagation();\n        ev.stopImmediatePropagation();\n        ev.preventDefault();\n    }\n};\n\n/**\n * @param {HTMLElement} target\n * @param {number} start\n * @param {number} end\n */\nconst changeSelection = async (target, start, end) => {\n    if (!isNil(start) && !isNil(target.selectionStart)) {\n        target.selectionStart = start;\n    }\n    if (!isNil(end) && !isNil(target.selectionEnd)) {\n        target.selectionEnd = end;\n    }\n};\n\n/**\n * @param {HTMLElement} target\n * @param {number} x\n */\nconst constrainScrollX = (target, x) => {\n    let { offsetWidth, scrollWidth } = target;\n    const document = getDocument(target);\n    if (target === document || target === document.documentElement) {\n        // <html> elements in iframes consider the width of the <iframe> element\n        const iframe = getParentFrame(target);\n        if (iframe) {\n            ({ offsetWidth } = iframe);\n        }\n    }\n    const maxScrollLeft = scrollWidth - offsetWidth;\n    const { direction } = getStyle(target);\n    const [min, max] = direction === \"rtl\" ? [-maxScrollLeft, 0] : [0, maxScrollLeft];\n    return $min($max(x, min), max);\n};\n\n/**\n * @param {HTMLElement} target\n * @param {number} y\n */\nconst constrainScrollY = (target, y) => {\n    let { offsetHeight, scrollHeight } = target;\n    const document = getDocument(target);\n    if (target === document || target === document.documentElement) {\n        // <html> elements in iframes consider the height of the <iframe> element\n        const iframe = getParentFrame(target);\n        if (iframe) {\n            ({ offsetHeight } = iframe);\n        }\n    }\n    return $min($max(y, 0), scrollHeight - offsetHeight);\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 * @template {EventTarget} T\n * @param {{\n *  target: T;\n *  events: EventType[];\n *  additionalEvents?: EventType[];\n *  callback?: (target: T) => any;\n *  options?: EventInit;\n * }} params\n */\nconst dispatchAndIgnore = async ({ target, events, additionalEvents = [], callback, options }) => {\n    for (const eventType of [...events, ...additionalEvents]) {\n        runTime.eventsToIgnore.push(eventType);\n    }\n    if (callback) {\n        callback(target);\n    }\n    for (const eventType of events) {\n        await dispatch(target, eventType, options);\n    }\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    // Buttons\n    buttons: 0,\n\n    // Modifier keys\n    modifierKeys: {},\n\n    /**\n     * Ignored events (\"select\" by default since it is sometimes dispatched by\n     * focusing an input).\n     * @type {EventType[]}\n     */\n    eventsToIgnore: [],\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), number]}\n */\nconst getEventConstructor = (eventType) => {\n    switch (eventType) {\n        // Mouse events\n        case \"dblclick\":\n        case \"mousedown\":\n        case \"mouseup\":\n        case \"mousemove\":\n        case \"mouseover\":\n        case \"mouseout\":\n            return [MouseEvent, mapMouseEvent, BUBBLES | CANCELABLE | VIEW];\n        case \"mouseenter\":\n        case \"mouseleave\":\n            return [MouseEvent, mapMouseEvent, VIEW];\n\n        // Pointer events\n        case \"auxclick\":\n        case \"click\":\n        case \"contextmenu\":\n        case \"pointerdown\":\n        case \"pointerup\":\n        case \"pointermove\":\n        case \"pointerover\":\n        case \"pointerout\":\n            return [PointerEvent, mapPointerEvent, BUBBLES | CANCELABLE | VIEW];\n        case \"pointerenter\":\n        case \"pointerleave\":\n        case \"pointercancel\":\n            return [PointerEvent, mapPointerEvent, VIEW];\n\n        // Focus events\n        case \"blur\":\n        case \"focus\":\n            return [FocusEvent, mapEvent];\n        case \"focusin\":\n        case \"focusout\":\n            return [FocusEvent, mapEvent, BUBBLES];\n\n        // Clipboard events\n        case \"cut\":\n        case \"copy\":\n        case \"paste\":\n            return [ClipboardEvent, mapEvent, BUBBLES];\n\n        // Keyboard events\n        case \"keydown\":\n        case \"keyup\":\n            return [KeyboardEvent, mapKeyboardEvent, BUBBLES | CANCELABLE | VIEW];\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, mapEvent, BUBBLES];\n\n        // Input events\n        case \"beforeinput\":\n            return [InputEvent, mapInputEvent, BUBBLES | CANCELABLE | VIEW];\n        case \"input\":\n            return [InputEvent, mapInputEvent, BUBBLES | VIEW];\n\n        // Composition events\n        case \"compositionstart\":\n        case \"compositionend\":\n            return [CompositionEvent, mapEvent, BUBBLES];\n\n        // Selection events\n        case \"select\":\n        case \"selectionchange\":\n            return [Event, mapEvent, BUBBLES];\n\n        // Touch events\n        case \"touchstart\":\n        case \"touchend\":\n        case \"touchmove\":\n            return [TouchEvent, mapTouchEvent, BUBBLES | CANCELABLE | VIEW];\n        case \"touchcancel\":\n            return [TouchEvent, mapTouchEvent, BUBBLES | VIEW];\n\n        // Resize events\n        case \"resize\":\n            return [Event, mapEvent];\n\n        // Submit events\n        case \"submit\":\n            return [SubmitEvent, mapEvent, BUBBLES | CANCELABLE];\n\n        // Wheel events\n        case \"wheel\":\n            return [WheelEvent, mapWheelEvent, BUBBLES | VIEW];\n\n        // Animation events\n        case \"animationcancel\":\n        case \"animationend\":\n        case \"animationiteration\":\n        case \"animationstart\": {\n            return [AnimationEvent, mapEvent, BUBBLES | CANCELABLE];\n        }\n\n        // Error events\n        case \"error\":\n            return [ErrorEvent, mapEvent];\n        case \"unhandledrejection\":\n            return [PromiseRejectionEvent, mapEvent, CANCELABLE];\n\n        // Unload events (BeforeUnloadEvent cannot be constructed)\n        case \"beforeunload\":\n            return [Event, mapEvent, CANCELABLE];\n        case \"unload\":\n            return [Event, mapEvent];\n\n        // Default: base Event constructor\n        default:\n            return [Event, mapEvent, BUBBLES];\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 * Redirects all 'submit' events to explicit network requests.\n *\n * This allows the `mockFetch` helper to take control over submit requests.\n *\n * @param {SubmitEvent} ev\n */\nconst redirectSubmit = (ev) => {\n    if (isPrevented(ev)) {\n        return;\n    }\n\n    ev.preventDefault();\n\n    /** @type {HTMLFormElement} */\n    const form = ev.target;\n\n    globalThis.fetch(form.action, {\n        method: form.method,\n        body: new FormData(form, ev.submitter),\n    });\n};\n\n/**\n * @param {PointerEventInit} eventInit\n * @param {boolean} toggle\n */\nconst registerButton = (eventInit, toggle) => {\n    let value = 0;\n    switch (eventInit.button) {\n        case btn.LEFT: {\n            // Main button (left button)\n            value = 1;\n            break;\n        }\n        case btn.MIDDLE: {\n            // Auxiliary button (middle button)\n            value = 4;\n            break;\n        }\n        case btn.RIGHT: {\n            // Secondary button (right button)\n            value = 2;\n            break;\n        }\n        case btn.BACK: {\n            // Fourth button (Browser Back)\n            value = 8;\n            break;\n        }\n        case btn.FORWARD: {\n            // Fifth button (Browser Forward)\n            value = 16;\n            break;\n        }\n    }\n\n    runTime.buttons = $max(runTime.buttons + (toggle ? value : -value), 0);\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 dispatchChange = () => target.value !== initialValue && dispatch(target, \"change\");\n\n    confirmAction &&= confirmAction.toLowerCase();\n    if (confirmAction === \"auto\") {\n        confirmAction = getTag(target) === \"input\" ? \"enter\" : \"blur\";\n    }\n    if (getTag(target) === \"input\") {\n        changeTargetListeners.push(\n            on(target, \"keydown\", (ev) => {\n                if (isPrevented(ev) || ev.key !== \"Enter\") {\n                    return;\n                }\n                removeChangeTargetListeners();\n                afterNextDispatch = dispatchChange;\n            })\n        );\n    } else if (confirmAction === \"enter\") {\n        throw new HootDomError(`\"enter\" confirm action is only supported on <input/> elements`);\n    }\n\n    changeTargetListeners.push(\n        on(target, \"blur\", () => {\n            removeChangeTargetListeners();\n            dispatchChange();\n        }),\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            runTime.modifierKeys.altKey = toggle;\n            break;\n        }\n        case \"Control\": {\n            runTime.modifierKeys.ctrlKey = toggle;\n            break;\n        }\n        case \"Meta\": {\n            runTime.modifierKeys.metaKey = toggle;\n            break;\n        }\n        case \"Shift\": {\n            runTime.modifierKeys.shiftKey = toggle;\n            break;\n        }\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 * @param {EventOptions} type\n */\nconst setupEvents = (type, options) => {\n    currentEventTypes.push(type);\n    $assign(currentEventInit, options?.eventInit);\n\n    return async () => {\n        for (const eventType in currentEventInit) {\n            delete currentEventInit[eventType];\n        }\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} pointerInit\n */\nconst triggerClick = async (target, pointerInit) => {\n    if (target.disabled) {\n        return;\n    }\n    const eventType = (pointerInit.button ?? 0) === btn.LEFT ? \"click\" : \"auxclick\";\n    const clickEvent = await dispatch(target, eventType, 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        await dispatchAndIgnore({\n            target: previous,\n            events: [\"blur\", \"focusout\"],\n            callback: (el) => el.blur(),\n            options: { relatedTarget: target },\n        });\n    }\n    if (isNodeFocusable(target)) {\n        const previousSelection = getStringSelection(target);\n        await dispatchAndIgnore({\n            target,\n            events: [\"focus\", \"focusin\"],\n            additionalEvents: [\"select\"],\n            callback: (el) => el.focus(),\n            options: { relatedTarget: previous },\n        });\n        if (previousSelection && previousSelection === getStringSelection(target)) {\n            changeSelection(target, target.value.length, 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} [options]\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    eventInit = { ...eventInit, ...currentEventInit.keydown };\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    const initialValue = target.value;\n    let inputData = null;\n    let inputType = null;\n    let nextSelectionEnd = null;\n    let nextSelectionStart = null;\n    let nextValue = initialValue;\n    let triggerSelect = false;\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                nextSelectionStart = nextSelectionEnd = $max(\n                    $min(selectionTarget, value.length),\n                    0\n                );\n                triggerSelect = shiftKey;\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                    nextSelectionStart = 0;\n                    nextSelectionEnd = target.value.length;\n                    triggerSelect = true;\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: btn.LEFT });\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 (initialValue !== nextValue) {\n        target.value = nextValue;\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    changeSelection(target, nextSelectionStart, nextSelectionEnd);\n    if (triggerSelect) {\n        await dispatchAndIgnore({\n            target,\n            events: [\"select\"],\n        });\n    }\n};\n\n/**\n * @param {EventTarget} target\n * @param {KeyboardEventInit} eventInit\n */\nconst _keyUp = async (target, eventInit) => {\n    eventInit = { ...eventInit, ...currentEventInit.keyup };\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: btn.LEFT });\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        ...currentEventInit.pointerdown,\n        button: options?.button || btn.LEFT,\n    };\n\n    registerButton(eventInit, true);\n\n    if (pointerDownTarget !== runTime.previousPointerDownTarget) {\n        runTime.clickCount = 0;\n    }\n\n    runTime.touchStartPosition = { ...runTime.position };\n    runTime.touchStartTimeOffset = globalThis.Date.now();\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 === btn.LEFT && !hasTouch() && pointerDownTarget.draggable) {\n        runTime.canStartDrag = true;\n    } else if (eventInit.button === btn.RIGHT) {\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 isLongTap = globalThis.Date.now() - runTime.touchStartTimeOffset > LONG_TAP_DELAY;\n    const pointerDownTarget = runTime.pointerDownTarget;\n    const eventInit = {\n        ...runTime.position,\n        ...currentEventInit.pointerup,\n        button: options?.button || btn.LEFT,\n    };\n\n    registerButton(eventInit, false);\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) || isLongTap)) {\n        // No further event is triggered:\n        // there was a swiping motion since the \"touchstart\" event\n        // or a long press was detected.\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        if (mouseEventInit.button === btn.LEFT) {\n            runTime.clickCount++;\n            if (!hasTouch() && runTime.clickCount % 2 === 0) {\n                await dispatch(actualTarget, \"dblclick\", mouseEventInit);\n            }\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 btn = {\n    LEFT: 0,\n    MIDDLE: 1,\n    RIGHT: 2,\n    BACK: 3,\n    FORWARD: 4,\n};\nconst CAPTURE = { capture: true };\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;\n\n/**\n * Ignore certain trusted events (dispatched by `focus()`, `scroll()`, etc.)\n * @type {[EventType, (event: Event) => any, AddEventListenerOptions][]}\n */\nconst GLOBAL_TRUSTED_EVENTS_CANCELERS = [\n    [\"blur\", cancelTrustedEvent, CAPTURE],\n    [\"focus\", cancelTrustedEvent, CAPTURE],\n    [\"focusin\", cancelTrustedEvent, CAPTURE],\n    [\"focusout\", cancelTrustedEvent, CAPTURE],\n    [\"scroll\", cancelTrustedEvent, CAPTURE],\n    [\"scrollend\", cancelTrustedEvent, CAPTURE],\n    [\"select\", cancelTrustedEvent, CAPTURE],\n];\n/**\n * Register file input on click & focus events\n * @type {[EventType, (event: Event) => any, AddEventListenerOptions][]}\n */\nconst GLOBAL_FILE_INPUT_REGISTERERS = [\n    [\"click\", registerFileInput, CAPTURE],\n    [\"focus\", registerFileInput, CAPTURE],\n];\n/**\n * Redirect events to other features\n * @type {[EventType, (event: Event) => any, AddEventListenerOptions][]}\n */\nconst GLOBAL_SUBMIT_FORWARDERS = [[\"submit\", redirectSubmit]];\n\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};\nconst LONG_TAP_DELAY = 500;\n\n/** @type {Record<string, Event[]>} */\nconst currentEvents = $create(null);\n/** @type {Record<EventType, EventInit>} */\nconst currentEventInit = $create(null);\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 = [];\n\n// Other global variables\nconst runTime = getDefaultRunTimeValue();\n\n//-----------------------------------------------------------------------------\n// Event init attributes mappers\n//-----------------------------------------------------------------------------\n\nconst BUBBLES = 0b1;\nconst CANCELABLE = 0b10;\nconst VIEW = 0b100;\n\n// Generic mappers\n// ---------------\n\n/**\n * - does not bubble\n * - cannot be canceled\n * @param {FullEventInit} eventInit\n */\nconst mapEvent = (eventInit) => eventInit;\n\n// Pointer, mouse & wheel event mappers\n// ------------------------------------\n\n/**\n * @param {FullEventInit<MouseEventInit>} eventInit\n */\nconst mapMouseEvent = (eventInit) => ({\n    button: -1,\n    buttons: runTime.buttons,\n    clientX: eventInit.clientX ?? eventInit.pageX ?? eventInit.screenX ?? 0,\n    clientY: eventInit.clientY ?? eventInit.pageY ?? eventInit.screenY ?? 0,\n    ...runTime.modifierKeys,\n    ...eventInit,\n});\n\n/**\n * @param {FullEventInit<PointerEventInit>} eventInit\n */\nconst mapPointerEvent = (eventInit) => ({\n    ...mapMouseEvent(eventInit),\n    button: btn.LEFT,\n    pointerId: 1,\n    pointerType: hasTouch() ? \"touch\" : \"mouse\",\n    ...eventInit,\n});\n\n/**\n * @param {FullEventInit<WheelEventInit>} eventInit\n */\nconst mapWheelEvent = (eventInit) => ({\n    ...mapMouseEvent(eventInit),\n    button: btn.LEFT,\n    ...eventInit,\n});\n\n// Touch event mappers\n// -------------------\n\n/**\n * @param {FullEventInit<TouchEventInit>} eventInit\n */\nconst mapTouchEvent = (eventInit) => {\n    const touches = eventInit.targetTouches ||\n        eventInit.touches || [new Touch({ identifier: 0, ...eventInit })];\n    return {\n        ...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// Keyboard & input event mappers\n// ------------------------------\n\n/**\n * @param {FullEventInit<InputEventInit>} eventInit\n */\nconst mapInputEvent = (eventInit) => ({\n    data: null,\n    isComposing: Boolean(runTime.isComposing),\n    ...eventInit,\n});\n\n/**\n * @param {FullEventInit<KeyboardEventInit>} eventInit\n */\nconst mapKeyboardEvent = (eventInit) => ({\n    isComposing: Boolean(runTime.isComposing),\n    ...runTime.modifierKeys,\n    ...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\", options);\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();\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\", options);\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();\n}\n\n/**\n * Performs a click sequence on the given {@link AsyncTarget}.\n *\n * The event sequence is as follows:\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\", options);\n    const element = queryFirst(await target, options);\n\n    await _implicitHover(element, options);\n    await _click(element, options);\n\n    return finalizeEvents();\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\", options);\n    const element = queryFirst(await target, options);\n\n    options = { ...options, button: btn.LEFT };\n    await _implicitHover(element, options);\n    await _click(element, options);\n    await _click(element, options);\n\n    return finalizeEvents();\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 * @template {HTMLBodyElementEventMap[T]} I\n * @param {EventTarget} target\n * @param {T} type\n * @param {Partial<I> | { eventInit: Record<T, Partial<I>> }} [eventInit]\n * @example\n *  await dispatch(document.querySelector(\"input\"), \"paste\"); // Dispatches a \"paste\" event on the given <input>\n * @returns {Promise<I>}\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    eventInit = { ...eventInit, ...currentEventInit[type] };\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    const [Constructor, processParams, flags] = getEventConstructor(type);\n    const params = processParams({\n        composed: true,\n        ...eventInit,\n        target,\n        type,\n    });\n    if (flags & BUBBLES) {\n        params.bubbles = true;\n    }\n    if (flags & CANCELABLE) {\n        params.cancelable = true;\n    }\n    if (flags & VIEW) {\n        params.view ||= getWindow(target);\n    }\n    const event = new Constructor(type, params);\n\n    target.dispatchEvent(event);\n    await Promise.resolve();\n\n    getCurrentEvents().push(event);\n\n    if (afterNextDispatch) {\n        const callback = afterNextDispatch;\n        afterNextDispatch = null;\n        await microTick().then(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\", options);\n            const element = getDocument().body;\n\n            // Reset buttons\n            runTime.buttons = 0;\n\n            await _press(element, { key: \"Escape\" });\n\n            dragEvents.push(...(await finalizeEvents()));\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\", options);\n\n            await _pointerUp(runTime.pointerTarget, options);\n\n            dragEvents.push(...(await finalizeEvents()));\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\", options);\n\n            await _hover(queryFirst(await to), options);\n\n            dragEvents.push(...(await finalizeEvents()));\n\n            return dragHelpers;\n        },\n        false\n    );\n\n    const finalizeEvents = setupEvents(\"drag & drop: start\", options);\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();\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\", options);\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();\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\", options);\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();\n}\n\n/**\n * Performs a hover sequence on the given {@link AsyncTarget}.\n *\n * The event sequence is as follows:\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\", options);\n    const element = queryFirst(await target, options);\n\n    await _hover(element, options);\n\n    return finalizeEvents();\n}\n\n/**\n * Performs a key down sequence on the current **active element**.\n *\n * The event sequence is as follows:\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\", options);\n    const eventInits = parseKeyStrokes(keyStrokes, options);\n    for (const eventInit of eventInits) {\n        await _keyDown(getActiveElement(), eventInit);\n    }\n\n    return finalizeEvents();\n}\n\n/**\n * Performs a key up sequence on the current **active element**.\n *\n * The event sequence is as follows:\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\", options);\n    const eventInits = parseKeyStrokes(keyStrokes, options);\n    for (const eventInit of eventInits) {\n        await _keyUp(getActiveElement(), eventInit);\n    }\n\n    return finalizeEvents();\n}\n\n/**\n * Performs a leave sequence on the current **window**.\n *\n * The event sequence is as follows:\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\", options);\n\n    await _hover(null, options);\n\n    return finalizeEvents();\n}\n\n/**\n * Performs a middle-click sequence 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 *  middleClick(\"button\"); // Middle-clicks on the first <button> element\n */\nexport async function middleClick(target, options) {\n    const finalizeEvents = setupEvents(\"middleClick\", options);\n    const element = queryFirst(await target, options);\n\n    options = { ...options, button: btn.MIDDLE };\n    await _implicitHover(element, options);\n    await _click(element, options);\n\n    return finalizeEvents();\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 follows:\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\", options);\n    const element = queryFirst(await target, options);\n\n    await _implicitHover(element, options);\n    await _pointerDown(element, options);\n\n    return finalizeEvents();\n}\n\n/**\n * Performs a pointer up on the given {@link AsyncTarget}.\n *\n * The event sequence is as follows:\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\", options);\n    const element = queryFirst(await target, options);\n\n    await _implicitHover(element, options);\n    await _pointerUp(element, options);\n\n    return finalizeEvents();\n}\n\n/**\n * Performs a keyboard event sequence on the current **active element**.\n *\n * The event sequence is as follows:\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\", options);\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();\n}\n\n/**\n * Performs a resize event sequence on the current **window**.\n *\n * The event sequence is as follows:\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\", options);\n    const [width, height] = parseDimensions(dimensions);\n\n    setDimensions(width, height);\n\n    await dispatch(getWindow(), \"resize\");\n\n    return finalizeEvents();\n}\n\n/**\n * Performs a right-click sequence 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 *  rightClick(\"button\"); // Middle-clicks on the first <button> element\n */\nexport async function rightClick(target, options) {\n    const finalizeEvents = setupEvents(\"rightClick\", options);\n    const element = queryFirst(await target, options);\n\n    options = { ...options, button: btn.RIGHT };\n    await _implicitHover(element, options);\n    await _click(element, options);\n\n    return finalizeEvents();\n}\n\n/**\n * Performs a scroll event sequence on the given {@link AsyncTarget}.\n *\n * The event sequence is as follows:\n *  - [desktop] `wheel`\n *  - `scroll`\n *\n * @param {AsyncTarget} target\n * @param {Position} position\n * @param {ScrollOptions} [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\", options);\n\n    const { force, initiator = \"wheel\", relative } = options || {};\n    /** @type {ScrollToOptions} */\n    const scrollTopOptions = {};\n    const element = queryFirst(await target, { scrollable: true, ...options });\n    let [x, y] = parsePosition(position);\n    if (relative) {\n        x += element.scrollLeft;\n        y += element.scrollTop;\n    }\n    if (!$isNaN(x)) {\n        const targetX = force ? x : constrainScrollX(element, x);\n        if (targetX !== element.scrollLeft) {\n            scrollTopOptions.left = targetX;\n        }\n    }\n    if (!$isNaN(y)) {\n        const targetY = force ? y : constrainScrollY(element, y);\n        if (targetY !== element.scrollTop) {\n            scrollTopOptions.top = targetY;\n        }\n    }\n    const keys = [];\n    if (initiator === \"keyboard\") {\n        if (x < element.scrollLeft) {\n            keys.push(\"ArrowRight\");\n        } else if (x > element.scrollLeft) {\n            keys.push(\"ArrowLeft\");\n        }\n        if (y < element.scrollTop) {\n            keys.push(\"ArrowDown\");\n        } else if (y > element.scrollTop) {\n            keys.push(\"ArrowUp\");\n        }\n        await Promise.all(keys.map((key) => _keyDown(key)));\n    } else if (!hasTouch() && initiator === \"wheel\") {\n        /** @type {WheelEventInit} */\n        const wheelEventInit = {};\n        if (!$isNaN(x)) {\n            wheelEventInit.deltaX = x - element.scrollLeft;\n        }\n        if (!$isNaN(y)) {\n            wheelEventInit.deltaY = y - element.scrollTop;\n        }\n        await dispatch(element, \"wheel\", wheelEventInit);\n    }\n    if (force || $values(scrollTopOptions).length) {\n        await dispatchAndIgnore({\n            target: element,\n            events: [\"scroll\", \"scrollend\"],\n            callback: (el) => el.scrollTo(scrollTopOptions),\n        });\n    }\n    if (initiator === \"keyboard\") {\n        await Promise.all(keys.map((key) => _keyUp(key)));\n    }\n\n    return finalizeEvents();\n}\n\n/**\n * Performs a selection event sequence current **active element**. This helper is\n * intended for `<select>` elements only.\n *\n * The event sequence is as follows:\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\", options);\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();\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\", options);\n\n    await _fill(runTime.fileInput, files, options);\n\n    runTime.fileInput = null;\n\n    return finalizeEvents();\n}\n\n/**\n * Sets the given value to the given \"input[type=range]\" {@link AsyncTarget}.\n *\n * The event sequence is as follows:\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\", options);\n    const element = queryFirst(await target, options);\n\n    await _implicitHover(element, options);\n    await _pointerDown(element, options);\n    await _fill(element, value, options);\n    await _pointerUp(element, options);\n\n    return finalizeEvents();\n}\n\n/**\n * @param {HTMLElement} target\n * @param {{\n *  allowSubmit?: boolean;\n *  allowTrustedEvents?: boolean;\n *  noFileInputRegistration?: boolean;\n * }} [options]\n */\nexport function setupEventActions(target, options) {\n    const eventHandlers = [];\n    if (!options?.allowTrustedEvents) {\n        eventHandlers.push(...GLOBAL_TRUSTED_EVENTS_CANCELERS);\n    }\n    if (!options?.noFileInputRegistration) {\n        eventHandlers.push(...GLOBAL_FILE_INPUT_REGISTERERS);\n    }\n    if (!options?.allowSubmit) {\n        eventHandlers.push(...GLOBAL_SUBMIT_FORWARDERS);\n    }\n    for (const [eventType, handler, options] of eventHandlers) {\n        window.addEventListener(eventType, handler, options);\n    }\n\n    const processedIframes = new WeakSet();\n    const observer = new MutationObserver((mutations) => {\n        for (const mutation of mutations) {\n            if (!mutation.addedNodes) {\n                continue;\n            }\n            for (const iframe of target.getElementsByTagName(\"iframe\")) {\n                if (processedIframes.has(iframe)) {\n                    continue;\n                }\n                processedIframes.add(iframe);\n                for (const [eventType, handler, options] of eventHandlers) {\n                    iframe.contentWindow.addEventListener(eventType, handler, options);\n                }\n            }\n        }\n    });\n\n    observer.observe(target, { childList: true, subtree: true });\n\n    return function cleanupEventActions() {\n        observer.disconnect();\n\n        if (runTime.pointerDownTimeout) {\n            globalThis.clearTimeout(runTime.pointerDownTimeout);\n        }\n\n        removeChangeTargetListeners();\n\n        for (const [eventType, handler, options] of eventHandlers) {\n            window.removeEventListener(eventType, handler, options);\n        }\n\n        // Runtime global variables\n        $assign(runTime, getDefaultRunTimeValue());\n    };\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\", options);\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();\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\", options);\n\n    await dispatch(getWindow(), \"beforeunload\");\n\n    return finalizeEvents();\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    parseInt,\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\n/**\n * Converts a given value to a **natural number** (or 0 if failing to do so).\n *\n * @param {unknown} value\n */\nconst parseNat = (value) => {\n    const int = parseInt(value, 10);\n    return int > 0 ? int : 0;\n};\n\nconst now = () => (freezed ? 0 : $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 = false;\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 * parseNat(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    ms = parseNat(ms);\n\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 {Promise<void>}\n */\nexport function animationFrame() {\n    return new Promise((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    allowTimers = false;\n    freezed = false;\n\n    cancelAllTimers();\n\n    // Wait for remaining async code to run\n    return delay();\n}\n\n/**\n * Returns a promise resolved after a given amount of milliseconds (default to 0).\n *\n * @param {number} [duration]\n * @returns {Promise<void>}\n * @example\n *  await delay(1000); // waits for 1 second\n */\nexport function delay(duration) {\n    return new Promise((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 new Promise(queueMicrotask);\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    ms = parseNat(ms);\n\n    const handler = () => {\n        if (allowTimers) {\n            intervalValues[1] = $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    ms = parseNat(ms);\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 * @returns {Promise<number>} time consumed by timers (in ms).\n */\nexport function runAllTimers() {\n    if (!timers.size) {\n        return 0;\n    }\n\n    const endts = $max(...[...timers.values()].map(([, init, delay]) => init + delay));\n    return advanceTime($ceil(endts - now()));\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    frameRate = parseNat(frameRate);\n    if (frameRate < 1 || frameRate > 1000) {\n        throw new Error(\"frame rate must be an number between 1 and 1000\");\n    }\n    frameDelay = 1000 / frameRate;\n}\n\nexport function setupTime() {\n    allowTimers = true;\n}\n\n/**\n * Returns a promise resolved after the next task tick.\n *\n * @returns {Promise<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 {Promise<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 Promise.resolve().then(() => result);\n    }\n\n    const timeout = $floor(options?.timeout ?? 200);\n    let handle;\n    let timeoutId;\n    let running = true;\n\n    return new Promise((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        handle = requestAnimationFrame(runCheck);\n        timeoutId = setTimeout(() => (running = false), timeout);\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\nimport * as dom from \"./helpers/dom\";\nimport * as events from \"./helpers/events\";\nimport { interactor } from \"./hoot_dom_utils\";\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    queryAll,\n    queryAllAttributes,\n    queryAllProperties,\n    queryAllRects,\n    queryAllTexts,\n    queryAllValues,\n    queryAttribute,\n    queryFirst,\n    queryOne,\n    queryRect,\n    queryText,\n    queryValue,\n} from \"./helpers/dom\";\nexport { on } from \"./helpers/events\";\nexport {\n    advanceFrame,\n    advanceTime,\n    animationFrame,\n    cancelAllTimers,\n    Deferred,\n    delay,\n    freezeTime,\n    microTick,\n    runAllTimers,\n    setFrameRate,\n    tick,\n    waitUntil,\n} from \"./helpers/time\";\n\n//-----------------------------------------------------------------------------\n// Interactors\n//-----------------------------------------------------------------------------\n\n// DOM\nexport const observe = interactor(\"query\", dom.observe);\nexport const waitFor = interactor(\"query\", dom.waitFor);\nexport const waitForNone = interactor(\"query\", dom.waitForNone);\n\n// Events\nexport const check = interactor(\"interaction\", events.check);\nexport const clear = interactor(\"interaction\", events.clear);\nexport const click = interactor(\"interaction\", events.click);\nexport const dblclick = interactor(\"interaction\", events.dblclick);\nexport const drag = interactor(\"interaction\", events.drag);\nexport const edit = interactor(\"interaction\", events.edit);\nexport const fill = interactor(\"interaction\", events.fill);\nexport const hover = interactor(\"interaction\", events.hover);\nexport const keyDown = interactor(\"interaction\", events.keyDown);\nexport const keyUp = interactor(\"interaction\", events.keyUp);\nexport const leave = interactor(\"interaction\", events.leave);\nexport const manuallyDispatchProgrammaticEvent = interactor(\"interaction\", events.dispatch);\nexport const middleClick = interactor(\"interaction\", events.middleClick);\nexport const pointerDown = interactor(\"interaction\", events.pointerDown);\nexport const pointerUp = interactor(\"interaction\", events.pointerUp);\nexport const press = interactor(\"interaction\", events.press);\nexport const resize = interactor(\"interaction\", events.resize);\nexport const rightClick = interactor(\"interaction\", events.rightClick);\nexport const scroll = interactor(\"interaction\", events.scroll);\nexport const select = interactor(\"interaction\", events.select);\nexport const setInputFiles = interactor(\"interaction\", events.setInputFiles);\nexport const setInputRange = interactor(\"interaction\", events.setInputRange);\nexport const uncheck = interactor(\"interaction\", events.uncheck);\nexport const unload = interactor(\"interaction\", events.unload);\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 * @typedef {[string, any[], any]} InteractionDetails\n *\n * @typedef {\"interaction\" | \"query\" | \"server\"} InteractionType\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    Object: { assign: $assign },\n    RegExp,\n    SyntaxError,\n} = globalThis;\n\n//-----------------------------------------------------------------------------\n// Internal\n//-----------------------------------------------------------------------------\n\nconst R_REGEX_PATTERN = /^\\/(.*)\\/([dgimsuvy]+)?$/;\n\nconst interactionBus = new EventTarget();\n\n//-----------------------------------------------------------------------------\n// Exports\n//-----------------------------------------------------------------------------\n\n/**\n * @param {Iterable<InteractionType>} types\n * @param {(event: CustomEvent<InteractionDetails>) => any} callback\n */\nexport function addInteractionListener(types, callback) {\n    for (const type of types) {\n        interactionBus.addEventListener(type, callback);\n    }\n\n    return function removeInteractionListener() {\n        for (const type of types) {\n            interactionBus.removeEventListener(type, callback);\n        }\n    };\n}\n\n/**\n * @param {InteractionType} type\n * @param {string} name\n * @param {any[]} args\n * @param {any} returnValue\n */\nexport function dispatchInteraction(type, name, args, returnValue) {\n    interactionBus.dispatchEvent(\n        new CustomEvent(type, {\n            detail: [name, args, returnValue],\n        })\n    );\n    return returnValue;\n}\n\nconst makeInteractorFn = (type, fn, name) =>\n    ({\n        [name](...args) {\n            const result = fn(...args);\n            if (result instanceof Promise) {\n                for (let i = 0; i < args.length; i++) {\n                    if (args[i] instanceof Promise) {\n                        // Get promise result for async arguments if possible\n                        args[i].then((result) => (args[i] = result));\n                    }\n                }\n                return result.then((promiseResult) =>\n                    dispatchInteraction(type, name, args, promiseResult)\n                );\n            } else {\n                return dispatchInteraction(type, name, args, result);\n            }\n        },\n    }[name]);\n\n/**\n * @template {(...args: any[]) => any} T\n * @param {InteractionType} type\n * @param {T} fn\n * @returns {T & {\n *  as: (name: string) => T;\n *  readonly silent: T;\n * }}\n */\nexport function interactor(type, fn) {\n    return $assign(makeInteractorFn(type, fn, fn.name), {\n        as(alias) {\n            return makeInteractorFn(type, fn, alias);\n        },\n        get silent() {\n            return fn;\n        },\n    });\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", "/** @odoo-module **/\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { Notebook } from \"@web/core/notebook/notebook\";\nimport { formatDateTime } from \"@web/core/l10n/dates\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { memoize } from \"@web/core/utils/functions\";\nimport { Component, onMounted, useState, markup } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\nimport { HtmlViewer } from \"@html_editor/fields/html_viewer\";\nimport { READONLY_MAIN_EMBEDDINGS } from \"@html_editor/others/embedded_components/embedding_sets\";\n\nconst { DateTime } = luxon;\n\nexport class HistoryDialog extends Component {\n    static template = \"html_editor.HistoryDialog\";\n    static components = { Dialog, HtmlViewer, Notebook };\n    static props = {\n        recordId: Number,\n        recordModel: String,\n        close: Function,\n        restoreRequested: Function,\n        historyMetadata: Array,\n        versionedFieldName: String,\n        title: { String, optional: true },\n        noContentHelper: { String, optional: true }, //Markup\n        embeddedComponents: { Array, optional: true },\n    };\n\n    static defaultProps = {\n        title: _t(\"History\"),\n        noContentHelper: markup(\"\"),\n        embeddedComponents: [...READONLY_MAIN_EMBEDDINGS],\n    };\n\n    state = useState({\n        revisionsData: [],\n        revisionContent: null,\n        revisionComparison: null,\n        revisionId: null,\n    });\n\n    setup() {\n        this.size = \"xl\";\n        this.title = this.props.title;\n        this.orm = useService(\"orm\");\n        this.notebookTabs = [_t(\"Content\"), _t(\"Comparison\")];\n\n        onMounted(() => this.init());\n    }\n\n    getConfig(value) {\n        return {\n            value: this.state[value],\n            embeddedComponents: this.props.embeddedComponents,\n        };\n    }\n\n    async init() {\n        this.state.revisionsData = this.props.historyMetadata;\n        await this.updateCurrentRevision(this.props.historyMetadata[0][\"revision_id\"]);\n    }\n\n    async updateCurrentRevision(revisionId) {\n        if (this.state.revisionId === revisionId) {\n            return;\n        }\n        this.env.services.ui.block();\n        this.state.revisionId = revisionId;\n        this.state.revisionContent = await this.getRevisionContent(revisionId);\n        this.state.revisionComparison = await this.getRevisionComparison(revisionId);\n        this.env.services.ui.unblock();\n    }\n\n    getRevisionComparison = memoize(\n        async function getRevisionComparison(revisionId) {\n            const comparison = await this.orm.call(\n                this.props.recordModel,\n                \"html_field_history_get_comparison_at_revision\",\n                [this.props.recordId, this.props.versionedFieldName, revisionId]\n            );\n            return markup(comparison);\n        }.bind(this)\n    );\n\n    getRevisionContent = memoize(\n        async function getRevisionContent(revisionId) {\n            const content = await this.orm.call(\n                this.props.recordModel,\n                \"html_field_history_get_content_at_revision\",\n                [this.props.recordId, this.props.versionedFieldName, revisionId]\n            );\n            return markup(content);\n        }.bind(this)\n    );\n\n    async _onRestoreRevisionClick() {\n        this.env.services.ui.block();\n        const restoredContent = await this.getRevisionContent(this.state.revisionId);\n        this.props.restoreRequested(restoredContent, this.props.close);\n        this.env.services.ui.unblock();\n    }\n\n    /**\n     * Getters\n     **/\n    getRevisionDate(revision) {\n        return formatDateTime(\n            DateTime.fromISO(revision[\"create_date\"], { zone: \"utc\" }).setZone(user.tz)\n        );\n    }\n}\n", "import {\n    containsAnyNonPhrasingContent,\n    isMediaElement,\n    isProtected,\n    isProtecting,\n} from \"@html_editor/utils/dom_info\";\nimport { Plugin } from \"../plugin\";\nimport { fillEmpty } from \"@html_editor/utils/dom\";\nimport {\n    BASE_CONTAINER_CLASS,\n    SUPPORTED_BASE_CONTAINER_NAMES,\n    baseContainerGlobalSelector,\n    createBaseContainer,\n} from \"../utils/base_container\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { selectElements } from \"@html_editor/utils/dom_traversal\";\n\nexport class BaseContainerPlugin extends Plugin {\n    static id = \"baseContainer\";\n    static shared = [\"createBaseContainer\", \"getDefaultNodeName\", \"isCandidateForBaseContainer\"];\n    /**\n     * Register one of the predicates for `invalid_for_base_container_predicates`\n     * as a property for optimization, see variants of `isCandidateForBaseContainer`.\n     */\n    hasNonPhrasingContentPredicate = (element) => {\n        return element?.nodeType === Node.ELEMENT_NODE && containsAnyNonPhrasingContent(element);\n    };\n    /**\n     * The `unsplittable` predicate for `invalid_for_base_container_predicates`\n     * is defined in this file and not in split_plugin because it has to be removed\n     * in a specific case: see `isCandidateForBaseContainerAllowUnsplittable`.\n     */\n    isUnsplittablePredicate = (element) => {\n        return this.getResource(\"unsplittable_node_predicates\").some((fn) => fn(element));\n    };\n    resources = {\n        clean_for_save_handlers: this.cleanForSave.bind(this),\n        // `baseContainer` normalization should occur after every other normalization\n        // because a `div` may only have the baseContainer identity if it does not\n        // already have another incompatible identity given by another plugin.\n        normalize_handlers: withSequence(Infinity, this.normalizeDivBaseContainers.bind(this)),\n        unsplittable_node_predicates: (node) => {\n            if (node.nodeName !== \"DIV\") {\n                return false;\n            }\n            return !this.isCandidateForBaseContainerAllowUnsplittable(node);\n        },\n        invalid_for_base_container_predicates: [\n            (node) =>\n                !node ||\n                node.nodeType !== Node.ELEMENT_NODE ||\n                !SUPPORTED_BASE_CONTAINER_NAMES.includes(node.tagName) ||\n                isProtected(node) ||\n                isProtecting(node) ||\n                isMediaElement(node),\n            this.isUnsplittablePredicate,\n            this.hasNonPhrasingContentPredicate,\n        ],\n        system_classes: [BASE_CONTAINER_CLASS],\n    };\n\n    createBaseContainer(nodeName = this.getDefaultNodeName()) {\n        return createBaseContainer(nodeName, this.document);\n    }\n\n    getDefaultNodeName() {\n        return this.config.baseContainer || \"P\";\n    }\n\n    /**\n     * Evaluate if an element is eligible to become a baseContainer (i.e. an\n     * unmarked div which could receive baseContainer attributes to inherit\n     * paragraph-like features).\n     *\n     * This function considers unsplittable and childNodes.\n     */\n    isCandidateForBaseContainer(element) {\n        return !this.getResource(\"invalid_for_base_container_predicates\").some((fn) => fn(element));\n    }\n\n    /**\n     * Evaluate if an element would be eligible to become a baseContainer\n     * without considering unsplittable.\n     *\n     * This function is only meant to be used during `unsplittable_node_predicates` to\n     * avoid an infinite loop:\n     * Considering a `DIV`,\n     * - During `unsplittable_node_predicates`, one predicate should return true\n     *   if the `DIV` is NOT a baseContainer candidate (Odoo specification),\n     *   therefore `invalid_for_base_container_predicates` should be evaluated.\n     * - During `invalid_for_base_container_predicates`, one predicate should\n     *   return true if the `DIV` is unsplittable, because a node has to be\n     *   splittable to use the featureSet associated with paragraphs.\n     * Each resource has to call the other. To avoid the issue, during\n     * `unsplittable_node_predicates`, the baseContainer predicate will execute\n     * all predicates for `invalid_for_base_container_predicates` except\n     * the one using `unsplittable_node_predicates`, since it is already being\n     * evaluated.\n     *\n     * In simpler terms:\n     * A `DIV` is unsplittable by default;\n     * UNLESS it is eligible to be a baseContainer => it becomes one;\n     * UNLESS it has to be unsplittable for an explicit reason (i.e. has class\n     * oe_unbreakable) => it stays unsplittable.\n     */\n    isCandidateForBaseContainerAllowUnsplittable(element) {\n        const predicates = new Set(this.getResource(\"invalid_for_base_container_predicates\"));\n        predicates.delete(this.isUnsplittablePredicate);\n        for (const predicate of predicates) {\n            if (predicate(element)) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    /**\n     * Evaluate if an element would be eligible to become a baseContainer\n     * without considering its childNodes.\n     *\n     * This function is only meant to be used internally, to avoid having to\n     * compute childNodes multiple times in more complex operations.\n     */\n    shallowIsCandidateForBaseContainer(element) {\n        const predicates = new Set(this.getResource(\"invalid_for_base_container_predicates\"));\n        predicates.delete(this.hasNonPhrasingContentPredicate);\n        for (const predicate of predicates) {\n            if (predicate(element)) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    cleanForSave({ root }) {\n        for (const baseContainer of selectElements(root, `.${BASE_CONTAINER_CLASS}`)) {\n            baseContainer.classList.remove(BASE_CONTAINER_CLASS);\n            if (baseContainer.classList.length === 0) {\n                baseContainer.removeAttribute(\"class\");\n            }\n        }\n    }\n\n    normalizeDivBaseContainers(element = this.editable) {\n        const newBaseContainers = [];\n        const divSelector = `div:not(.${BASE_CONTAINER_CLASS})`;\n        const targets = [...element.querySelectorAll(divSelector)];\n        if (element.matches(divSelector)) {\n            targets.unshift(element);\n        }\n        for (const div of targets) {\n            if (\n                // Ensure that newly created `div` baseContainers are never themselves\n                // children of a baseContainer. BaseContainers should always only\n                // contain phrasing content (even `div`), because they could be\n                // converted to an element which can actually only contain phrasing\n                // content. In practice a div should never be a child of a\n                // baseContainer, since a baseContainer should only contain\n                // phrasingContent.\n                !div.parentElement?.matches(baseContainerGlobalSelector) &&\n                this.shallowIsCandidateForBaseContainer(div) &&\n                !containsAnyNonPhrasingContent(div)\n            ) {\n                div.classList.add(BASE_CONTAINER_CLASS);\n                newBaseContainers.push(div);\n                fillEmpty(div);\n            }\n        }\n    }\n}\n", "import { isTextNode, isParagraphRelatedElement } from \"../utils/dom_info\";\nimport { Plugin } from \"../plugin\";\nimport { closestBlock, isBlock } from \"../utils/blocks\";\nimport { unwrapContents, wrapInlinesInBlocks, splitTextNode } from \"../utils/dom\";\nimport { ancestors, childNodes, closestElement } from \"../utils/dom_traversal\";\nimport { parseHTML } from \"../utils/html\";\nimport {\n    baseContainerGlobalSelector,\n    getBaseContainerSelector,\n} from \"@html_editor/utils/base_container\";\nimport { DIRECTIONS } from \"../utils/position\";\n\n/**\n * @typedef { import(\"./selection_plugin\").EditorSelection } EditorSelection\n */\n\nconst CLIPBOARD_BLACKLISTS = {\n    unwrap: [\n        // These elements' children will be unwrapped.\n        \".Apple-interchange-newline\",\n        \"DIV\", // DIV is unwrapped unless eligible to be a baseContainer, see cleanForPaste\n    ],\n    remove: [\"META\", \"STYLE\", \"SCRIPT\"], // These elements will be removed along with their children.\n};\nexport const CLIPBOARD_WHITELISTS = {\n    nodes: [\n        // Style\n        \"P\",\n        \"H1\",\n        \"H2\",\n        \"H3\",\n        \"H4\",\n        \"H5\",\n        \"H6\",\n        \"BLOCKQUOTE\",\n        \"PRE\",\n        // List\n        \"UL\",\n        \"OL\",\n        \"LI\",\n        // Inline style\n        \"I\",\n        \"B\",\n        \"U\",\n        \"S\",\n        \"EM\",\n        \"FONT\",\n        \"STRONG\",\n        // Table\n        \"TABLE\",\n        \"THEAD\",\n        \"TH\",\n        \"TBODY\",\n        \"TR\",\n        \"TD\",\n        // Miscellaneous\n        \"IMG\",\n        \"BR\",\n        \"A\",\n        \".fa\",\n    ],\n    classes: [\n        // Media\n        /^float-/,\n        \"d-block\",\n        \"mx-auto\",\n        \"img-fluid\",\n        \"img-thumbnail\",\n        \"rounded\",\n        \"rounded-circle\",\n        \"table\",\n        \"table-bordered\",\n        /^padding-/,\n        /^shadow/,\n        // Odoo colors\n        /^text-o-/,\n        /^bg-o-/,\n        // Odoo lists\n        \"o_checked\",\n        \"o_checklist\",\n        \"oe-nested\",\n        // Miscellaneous\n        /^btn/,\n        /^fa/,\n    ],\n    attributes: [\"class\", \"href\", \"src\", \"target\"],\n    styledTags: [\"SPAN\", \"B\", \"STRONG\", \"I\", \"S\", \"U\", \"FONT\", \"TD\"],\n};\n\nconst ONLY_LINK_REGEX = /^(https?:\\/\\/)?([\\w-]+\\.)+[\\w-]+(\\/[\\w-./?%&=]*)?$/i;\n\n/**\n * @typedef {Object} ClipboardShared\n * @property {ClipboardPlugin['pasteText']} pasteText\n */\n\nexport class ClipboardPlugin extends Plugin {\n    static id = \"clipboard\";\n    static dependencies = [\n        \"baseContainer\",\n        \"dom\",\n        \"selection\",\n        \"sanitize\",\n        \"history\",\n        \"split\",\n        \"delete\",\n        \"lineBreak\",\n    ];\n    static shared = [\"pasteText\"];\n\n    setup() {\n        this.addDomListener(this.editable, \"copy\", this.onCopy);\n        this.addDomListener(this.editable, \"cut\", this.onCut);\n        this.addDomListener(this.editable, \"paste\", this.onPaste);\n        this.addDomListener(this.editable, \"dragstart\", this.onDragStart);\n        this.addDomListener(this.editable, \"drop\", this.onDrop);\n    }\n\n    onCut(ev) {\n        this.onCopy(ev);\n        this.dependencies.history.stageSelection();\n        this.dependencies.delete.deleteSelection();\n        this.dependencies.history.addStep();\n    }\n\n    /**\n     * @param {ClipboardEvent} ev\n     */\n    onCopy(ev) {\n        ev.preventDefault();\n        const selection = this.dependencies.selection.getEditableSelection();\n        const commonAncestor = selection.commonAncestorContainer;\n        if (commonAncestor && commonAncestor.nodeType === Node.ELEMENT_NODE) {\n            this.dispatchTo(\"clean_handlers\", commonAncestor);\n        }\n        let clonedContents = selection.cloneContents();\n        if (!clonedContents.hasChildNodes()) {\n            if (commonAncestor && commonAncestor.nodeType === Node.ELEMENT_NODE) {\n                this.dispatchTo(\"normalize_handlers\", commonAncestor);\n            }\n            return;\n        }\n        // Repair the copied range.\n        if (clonedContents.firstChild.nodeName === \"LI\") {\n            const list = selection.commonAncestorContainer.cloneNode();\n            list.replaceChildren(...childNodes(clonedContents));\n            clonedContents = list;\n        }\n        if (\n            clonedContents.firstChild.nodeName === \"TR\" ||\n            clonedContents.firstChild.nodeName === \"TD\"\n        ) {\n            // We enter this case only if selection is within single table.\n            const table = closestElement(selection.commonAncestorContainer, \"table\");\n            const tableClone = table.cloneNode(true);\n            // A table is considered fully selected if it is nested inside a\n            // cell that is itself selected, or if all its own cells are\n            // selected.\n            const isTableFullySelected =\n                (table.parentElement &&\n                    !!closestElement(table.parentElement, \"td.o_selected_td\")) ||\n                [...table.querySelectorAll(\"td\")]\n                    .filter((td) => closestElement(td, \"table\") === table)\n                    .every((td) => td.classList.contains(\"o_selected_td\"));\n            if (!isTableFullySelected) {\n                for (const td of tableClone.querySelectorAll(\"td:not(.o_selected_td)\")) {\n                    if (closestElement(td, \"table\") === tableClone) {\n                        // ignore nested\n                        td.remove();\n                    }\n                }\n                const trsWithoutTd = Array.from(tableClone.querySelectorAll(\"tr\")).filter(\n                    (row) => !row.querySelector(\"td\")\n                );\n                for (const tr of trsWithoutTd) {\n                    if (closestElement(tr, \"table\") === tableClone) {\n                        // ignore nested\n                        tr.remove();\n                    }\n                }\n            }\n            // If it is fully selected, clone the whole table rather than\n            // just its rows.\n            clonedContents = tableClone;\n        }\n        const startTable = closestElement(selection.startContainer, \"table\");\n        if (clonedContents.firstChild.nodeName === \"TABLE\" && startTable) {\n            // Make sure the full leading table is copied.\n            clonedContents.firstChild.after(startTable.cloneNode(true));\n            clonedContents.firstChild.remove();\n        }\n        const endTable = closestElement(selection.endContainer, \"table\");\n        if (clonedContents.lastChild.nodeName === \"TABLE\" && endTable) {\n            // Make sure the full trailing table is copied.\n            clonedContents.lastChild.before(endTable.cloneNode(true));\n            clonedContents.lastChild.remove();\n        }\n        const commonAncestorElement = closestElement(selection.commonAncestorContainer);\n        if (commonAncestorElement && !isBlock(clonedContents.firstChild)) {\n            // Get the list of ancestor elements starting from the provided\n            // commonAncestorElement up to the block-level element.\n            const blockEl = closestBlock(commonAncestorElement);\n            const ancestorsList = [\n                commonAncestorElement,\n                ...ancestors(commonAncestorElement, blockEl),\n            ];\n            // Wrap rangeContent with clones of their ancestors to keep the styles.\n            for (const ancestor of ancestorsList) {\n                // Keep the formatting by keeping inline ancestors and paragraph\n                // related ones like headings etc.\n                if (!isBlock(ancestor) || isParagraphRelatedElement(ancestor)) {\n                    const clone = ancestor.cloneNode();\n                    clone.append(...childNodes(clonedContents));\n                    clonedContents.appendChild(clone);\n                }\n            }\n        }\n        const dataHtmlElement = this.document.createElement(\"data\");\n        dataHtmlElement.append(clonedContents);\n        const odooHtml = dataHtmlElement.innerHTML;\n        const odooText = selection.textContent();\n        ev.clipboardData.setData(\"text/plain\", odooText);\n        ev.clipboardData.setData(\"text/html\", odooHtml);\n        ev.clipboardData.setData(\"application/vnd.odoo.odoo-editor\", odooHtml);\n        if (commonAncestor && commonAncestor.nodeType === Node.ELEMENT_NODE) {\n            this.dispatchTo(\"normalize_handlers\", commonAncestor);\n        }\n    }\n\n    /**\n     * Handle safe pasting of html or plain text into the editor.\n     */\n    onPaste(ev) {\n        let selection = this.dependencies.selection.getEditableSelection();\n        if (!selection.anchorNode.isConnected) {\n            return;\n        }\n        ev.preventDefault();\n\n        this.dependencies.history.stageSelection();\n\n        this.dispatchTo(\"before_paste_handlers\", selection);\n        // refresh selection after potential changes from `before_paste` handlers\n        selection = this.dependencies.selection.getEditableSelection();\n\n        this.handlePasteUnsupportedHtml(selection, ev.clipboardData) ||\n            this.handlePasteOdooEditorHtml(ev.clipboardData) ||\n            this.handlePasteHtml(selection, ev.clipboardData) ||\n            this.handlePasteText(selection, ev.clipboardData);\n\n        this.dependencies.history.addStep();\n    }\n    /**\n     * @param {EditorSelection} selection\n     * @param {DataTransfer} clipboardData\n     */\n    handlePasteUnsupportedHtml(selection, clipboardData) {\n        const targetSupportsHtmlContent = isHtmlContentSupported(selection.anchorNode);\n        if (!targetSupportsHtmlContent) {\n            const text = clipboardData.getData(\"text/plain\");\n            this.dependencies.dom.insert(text);\n            return true;\n        }\n    }\n    /**\n     * @param {DataTransfer} clipboardData\n     */\n    handlePasteOdooEditorHtml(clipboardData) {\n        const odooEditorHtml = clipboardData.getData(\"application/vnd.odoo.odoo-editor\");\n        const textContent = clipboardData.getData(\"text/plain\");\n        if (ONLY_LINK_REGEX.test(textContent)) {\n            return false;\n        }\n        if (odooEditorHtml) {\n            const fragment = parseHTML(this.document, odooEditorHtml);\n            this.dependencies.sanitize.sanitize(fragment);\n            if (fragment.hasChildNodes()) {\n                this.dependencies.dom.insert(fragment);\n            }\n            return true;\n        }\n    }\n    /**\n     * @param {EditorSelection} selection\n     * @param {DataTransfer} clipboardData\n     */\n    handlePasteHtml(selection, clipboardData) {\n        const files = getImageFiles(clipboardData);\n        const clipboardHtml = clipboardData.getData(\"text/html\");\n        const textContent = clipboardData.getData(\"text/plain\");\n        if (ONLY_LINK_REGEX.test(textContent)) {\n            return false;\n        }\n        if (files.length || clipboardHtml) {\n            const clipboardElem = this.prepareClipboardData(clipboardHtml);\n            // @phoenix @todo: should it be handled in table plugin?\n            // When copy pasting a table from the outside, a picture of the\n            // table can be included in the clipboard as an image file. In that\n            // particular case the html table is given a higher priority than\n            // the clipboard picture.\n            if (files.length && !clipboardElem.querySelector(\"table\")) {\n                // @phoenix @todo: should it be handled in image plugin?\n                return this.addImagesFiles(files).then((html) => {\n                    this.dependencies.dom.insert(html);\n                    this.dependencies.history.addStep();\n                });\n            } else {\n                if (closestElement(selection.anchorNode, \"a\")) {\n                    this.dependencies.dom.insert(clipboardElem.textContent);\n                } else {\n                    this.dependencies.dom.insert(clipboardElem);\n                }\n            }\n            return true;\n        }\n    }\n    /**\n     * @param {EditorSelection} selection\n     * @param {DataTransfer} clipboardData\n     */\n    handlePasteText(selection, clipboardData) {\n        const text = clipboardData.getData(\"text/plain\");\n        if (this.delegateTo(\"paste_text_overrides\", selection, text)) {\n            return;\n        } else {\n            this.pasteText(selection, text);\n        }\n    }\n    /**\n     * @param {EditorSelection} selection\n     * @param {string} text\n     */\n    pasteText(selection, text) {\n        const textFragments = text.split(/\\r?\\n/);\n        let textIndex = 1;\n        for (const textFragment of textFragments) {\n            // Replace consecutive spaces by alternating nbsp.\n            const modifiedTextFragment = textFragment.replace(/( {2,})/g, (match) => {\n                let alertnateValue = false;\n                return match.replace(/ /g, () => {\n                    alertnateValue = !alertnateValue;\n                    const replaceContent = alertnateValue ? \"\\u00A0\" : \" \";\n                    return replaceContent;\n                });\n            });\n            this.dependencies.dom.insert(modifiedTextFragment);\n            // The selection must be updated after calling insert, as the insertion\n            // process modifies the selection.\n            selection = this.dependencies.selection.getEditableSelection();\n            if (textIndex < textFragments.length) {\n                // Break line by inserting new paragraph and\n                // remove current paragraph's bottom margin.\n                const block = closestBlock(selection.anchorNode);\n                if (\n                    this.dependencies.split.isUnsplittable(block) ||\n                    closestElement(selection.anchorNode).tagName === \"PRE\"\n                ) {\n                    this.dependencies.lineBreak.insertLineBreak();\n                } else {\n                    const [blockBefore] = this.dependencies.split.splitBlock();\n                    if (\n                        block &&\n                        block.matches(baseContainerGlobalSelector) &&\n                        blockBefore &&\n                        !blockBefore.matches(getBaseContainerSelector(\"DIV\"))\n                    ) {\n                        // Do something only if blockBefore is not a DIV (which is the no-margin option)\n                        // replace blockBefore by a DIV.\n                        const div = this.dependencies.baseContainer.createBaseContainer(\"DIV\");\n                        const cursors = this.dependencies.selection.preserveSelection();\n                        blockBefore.before(div);\n                        div.replaceChildren(...childNodes(blockBefore));\n                        blockBefore.remove();\n                        cursors.remapNode(blockBefore, div).restore();\n                    }\n                    selection = this.dependencies.selection.getEditableSelection();\n                }\n            }\n            textIndex++;\n        }\n    }\n\n    /**\n     * Prepare clipboard data (text/html) for safe pasting into the editor.\n     *\n     * @private\n     * @param {string} clipboardData\n     * @returns {DocumentFragment}\n     */\n    prepareClipboardData(clipboardData) {\n        const fragment = parseHTML(this.document, clipboardData);\n        this.dependencies.sanitize.sanitize(fragment);\n        const container = this.document.createElement(\"fake-container\");\n        container.append(fragment);\n\n        for (const tableElement of container.querySelectorAll(\"table\")) {\n            tableElement.classList.add(\"table\", \"table-bordered\", \"o_table\");\n        }\n\n        // todo: should it be in its own plugin ?\n        const progId = container.querySelector('meta[name=\"ProgId\"]');\n        if (progId && progId.content === \"Excel.Sheet\") {\n            // Microsoft Excel keeps table style in a <style> tag with custom\n            // classes. The following lines parse that style and apply it to the\n            // style attribute of <td> tags with matching classes.\n            const xlStylesheet = container.querySelector(\"style\");\n            const xlNodes = container.querySelectorAll(\"[class*=xl],[class*=font]\");\n            for (const xlNode of xlNodes) {\n                for (const xlClass of xlNode.classList) {\n                    // Regex captures a CSS rule definition for that xlClass.\n                    const xlStyle = xlStylesheet.textContent\n                        .match(`.${xlClass}[^{]*{(?<xlStyle>[^}]*)}`)\n                        .groups.xlStyle.replace(\"background:\", \"background-color:\");\n                    xlNode.setAttribute(\"style\", xlNode.style.cssText + \";\" + xlStyle);\n                }\n            }\n        }\n        const childContent = childNodes(container);\n        for (const child of childContent) {\n            this.cleanForPaste(child);\n        }\n        // Identify the closest baseContainer from the selection. This will\n        // determine which baseContainer will be used by default for the\n        // clipboard content if it has to be modified.\n        const selection = this.dependencies.selection.getEditableSelection();\n        const closestBaseContainer =\n            selection.anchorNode &&\n            closestElement(selection.anchorNode, baseContainerGlobalSelector);\n        // Force inline nodes at the root of the container into separate `baseContainers`\n        // elements. This is a tradeoff to ensure some features that rely on\n        // nodes having a parent (e.g. convert to list, title, etc.) can work\n        // properly on such nodes without having to actually handle that\n        // particular case in all of those functions. In fact, this case cannot\n        // happen on a new document created using this editor, but will happen\n        // instantly when editing a document that was created from Etherpad.\n        wrapInlinesInBlocks(container, {\n            baseContainerNodeName:\n                closestBaseContainer?.nodeName ||\n                this.dependencies.baseContainer.getDefaultNodeName(),\n        });\n        const result = this.document.createDocumentFragment();\n        result.replaceChildren(...childNodes(container));\n\n        // Split elements containing <br> into separate elements for each line.\n        const brs = result.querySelectorAll(\"br\");\n        for (const br of brs) {\n            const block = closestBlock(br);\n            if (\n                (isParagraphRelatedElement(block) ||\n                    this.dependencies.baseContainer.isCandidateForBaseContainer(block)) &&\n                // TODO specific exception for \"PRE\" to keep everything inside one PRE.\n                // Consider removing this if PRE is to be used as a paragraph.\n                block.nodeName !== \"PRE\" &&\n                !block.closest(\"li\")\n            ) {\n                // A linebreak at the beginning of a block is an empty line.\n                const isEmptyLine = block.firstChild.nodeName === \"BR\";\n                // Split blocks around it until only the BR remains in the\n                // block.\n                const remainingBrContainer = this.dependencies.split.splitAroundUntil(br, block);\n                // Remove the container unless it represented an empty line.\n                if (!isEmptyLine) {\n                    remainingBrContainer.remove();\n                }\n            }\n        }\n        return result;\n    }\n    /**\n     * Clean a node for safely pasting. Cleaning an element involves unwrapping\n     * its contents if it's an illegal (blacklisted or not whitelisted) element,\n     * or removing its illegal attributes and classes.\n     *\n     * @param {Node} node\n     */\n    cleanForPaste(node) {\n        if (\n            !this.isWhitelisted(node) ||\n            this.isBlacklisted(node) ||\n            // Google Docs have their html inside a B tag with custom id.\n            (node.id && node.id.startsWith(\"docs-internal-guid\"))\n        ) {\n            if (!node.matches || node.matches(CLIPBOARD_BLACKLISTS.remove.join(\",\"))) {\n                node.remove();\n            } else {\n                let childrenNodes;\n                if (node.nodeName === \"DIV\") {\n                    if (this.dependencies.baseContainer.isCandidateForBaseContainer(node)) {\n                        childrenNodes = childNodes(node);\n                    } else {\n                        childrenNodes = unwrapContents(node);\n                    }\n                } else {\n                    // Unwrap the illegal node's contents.\n                    childrenNodes = unwrapContents(node);\n                }\n                for (const child of childrenNodes) {\n                    this.cleanForPaste(child);\n                }\n            }\n        } else if (node.nodeType !== Node.TEXT_NODE) {\n            if (node.nodeName === \"TD\") {\n                if (node.hasAttribute(\"bgcolor\") && !node.style[\"background-color\"]) {\n                    node.style[\"background-color\"] = node.getAttribute(\"bgcolor\");\n                }\n            } else if (node.nodeName === \"FONT\") {\n                // FONT tags have some style information in custom attributes,\n                // this maps them to the style attribute.\n                if (node.hasAttribute(\"color\") && !node.style[\"color\"]) {\n                    node.style[\"color\"] = node.getAttribute(\"color\");\n                }\n                if (node.hasAttribute(\"size\") && !node.style[\"font-size\"]) {\n                    // FONT size uses non-standard numeric values.\n                    node.style[\"font-size\"] = +node.getAttribute(\"size\") + 10 + \"pt\";\n                }\n            } else if (\n                [\"S\", \"U\"].includes(node.nodeName) &&\n                childNodes(node).length === 1 &&\n                node.firstChild.nodeName === \"FONT\"\n            ) {\n                // S and U tags sometimes contain FONT tags. We prefer the\n                // strike to adopt the style of the text, so we invert them.\n                const fontNode = node.firstChild;\n                node.before(fontNode);\n                node.replaceChildren(...childNodes(fontNode));\n                fontNode.appendChild(node);\n            } else if (\n                node.nodeName === \"IMG\" &&\n                node.getAttribute(\"aria-roledescription\") === \"checkbox\"\n            ) {\n                const checklist = node.closest(\"ul\");\n                const closestLi = node.closest(\"li\");\n                if (checklist) {\n                    checklist.classList.add(\"o_checklist\");\n                    if (node.getAttribute(\"alt\") === \"checked\") {\n                        closestLi.classList.add(\"o_checked\");\n                    }\n                    node.remove();\n                    node = checklist;\n                }\n            }\n            // Remove all illegal attributes and classes from the node, then\n            // clean its children.\n            for (const attribute of [...node.attributes]) {\n                // Keep allowed styles on nodes with allowed tags.\n                // todo: should the whitelist be a resource?\n                if (\n                    CLIPBOARD_WHITELISTS.styledTags.includes(node.nodeName) &&\n                    attribute.name === \"style\"\n                ) {\n                    node.removeAttribute(attribute.name);\n                    if ([\"SPAN\", \"FONT\"].includes(node.tagName)) {\n                        for (const unwrappedNode of unwrapContents(node)) {\n                            this.cleanForPaste(unwrappedNode);\n                        }\n                    }\n                } else if (!this.isWhitelisted(attribute)) {\n                    node.removeAttribute(attribute.name);\n                }\n            }\n            for (const klass of [...node.classList]) {\n                if (!this.isWhitelisted(klass)) {\n                    node.classList.remove(klass);\n                }\n            }\n            for (const child of childNodes(node)) {\n                this.cleanForPaste(child);\n            }\n        }\n    }\n    /**\n     * Return true if the given attribute, class or node is whitelisted for\n     * pasting, false otherwise.\n     *\n     * @private\n     * @param {Attr | string | Node} item\n     * @returns {boolean}\n     */\n    isWhitelisted(item) {\n        if (item instanceof Attr) {\n            return CLIPBOARD_WHITELISTS.attributes.includes(item.name);\n        } else if (typeof item === \"string\") {\n            return CLIPBOARD_WHITELISTS.classes.some((okClass) =>\n                okClass instanceof RegExp ? okClass.test(item) : okClass === item\n            );\n        } else {\n            return isTextNode(item) || item.matches?.(CLIPBOARD_WHITELISTS.nodes.join(\",\"));\n        }\n    }\n    /**\n     * Return true if the given node is blacklisted for pasting, false\n     * otherwise.\n     *\n     * @private\n     * @param {Node} node\n     * @returns {boolean}\n     */\n    isBlacklisted(node) {\n        return (\n            !isTextNode(node) &&\n            node.matches([].concat(...Object.values(CLIPBOARD_BLACKLISTS)).join(\",\"))\n        );\n    }\n\n    /**\n     * @param {DragEvent} ev\n     */\n    onDragStart(ev) {\n        if (ev.target.nodeName === \"IMG\") {\n            this.dragImage = ev.target instanceof HTMLElement && ev.target;\n            ev.dataTransfer.setData(\n                \"application/vnd.odoo.odoo-editor-node\",\n                this.dragImage.outerHTML\n            );\n        }\n    }\n    /**\n     * Handle safe dropping of html into the editor.\n     *\n     * @param {DragEvent} ev\n     */\n    async onDrop(ev) {\n        ev.preventDefault();\n        if (!isHtmlContentSupported(ev.target)) {\n            return;\n        }\n        const selection = this.dependencies.selection.getEditableSelection();\n        const nodeToSplit =\n            selection.direction === DIRECTIONS.RIGHT ? selection.focusNode : selection.anchorNode;\n        const offsetToSplit =\n            selection.direction === DIRECTIONS.RIGHT\n                ? selection.focusOffset\n                : selection.anchorOffset;\n        if (nodeToSplit.nodeType === Node.TEXT_NODE && !selection.isCollapsed) {\n            const selectionToRestore = this.dependencies.selection.preserveSelection();\n            // Split the text node beforehand to ensure the insertion offset\n            // remains correct after deleting the selection.\n            splitTextNode(nodeToSplit, offsetToSplit, DIRECTIONS.LEFT);\n            selectionToRestore.restore();\n        }\n\n        const dataTransfer = (ev.originalEvent || ev).dataTransfer;\n        const imageNodeHTML = ev.dataTransfer.getData(\"application/vnd.odoo.odoo-editor-node\");\n        const image =\n            imageNodeHTML &&\n            this.dragImage &&\n            imageNodeHTML === this.dragImage.outerHTML &&\n            this.dragImage;\n\n        const fileTransferItems = getImageFiles(dataTransfer);\n        const htmlTransferItem = [...dataTransfer.items].find((item) => item.type === \"text/html\");\n        if (image || fileTransferItems.length || htmlTransferItem) {\n            if (this.document.caretPositionFromPoint) {\n                const range = this.document.caretPositionFromPoint(ev.clientX, ev.clientY);\n                this.dependencies.delete.deleteSelection();\n                this.dependencies.selection.setSelection({\n                    anchorNode: range.offsetNode,\n                    anchorOffset: range.offset,\n                });\n            } else if (this.document.caretRangeFromPoint) {\n                const range = this.document.caretRangeFromPoint(ev.clientX, ev.clientY);\n                this.dependencies.delete.deleteSelection();\n                this.dependencies.selection.setSelection({\n                    anchorNode: range.startContainer,\n                    anchorOffset: range.startOffset,\n                });\n            }\n        }\n        if (image) {\n            const fragment = this.document.createDocumentFragment();\n            fragment.append(image);\n            this.dependencies.dom.insert(fragment);\n            this.dependencies.history.addStep();\n        } else if (fileTransferItems.length) {\n            const html = await this.addImagesFiles(fileTransferItems);\n            this.dependencies.dom.insert(html);\n            this.dependencies.history.addStep();\n        } else if (htmlTransferItem) {\n            htmlTransferItem.getAsString((pastedText) => {\n                this.dependencies.dom.insert(this.prepareClipboardData(pastedText));\n                this.dependencies.history.addStep();\n            });\n        }\n    }\n    // @phoenix @todo: move to image or image paste plugin?\n    /**\n     * Add images inside the editable at the current selection.\n     *\n     * @param {File[]} imageFiles\n     */\n    async addImagesFiles(imageFiles) {\n        const promises = [];\n        for (const imageFile of imageFiles) {\n            const imageNode = this.document.createElement(\"img\");\n            imageNode.classList.add(\"img-fluid\");\n            // Mark images as having to be saved as attachments.\n            if (this.config.dropImageAsAttachment) {\n                imageNode.classList.add(\"o_b64_image_to_save\");\n            }\n            imageNode.dataset.fileName = imageFile.name;\n            promises.push(\n                getImageUrl(imageFile).then((url) => {\n                    imageNode.src = url;\n                    return imageNode;\n                })\n            );\n        }\n        const nodes = await Promise.all(promises);\n        const fragment = this.document.createDocumentFragment();\n        fragment.append(...nodes);\n        return fragment;\n    }\n}\n\n/**\n * @param {DataTransfer} dataTransfer\n */\nfunction getImageFiles(dataTransfer) {\n    return [...dataTransfer.items]\n        .filter((item) => item.kind === \"file\" && item.type.includes(\"image/\"))\n        .map((item) => item.getAsFile());\n}\n/**\n * @param {File} file\n */\nfunction getImageUrl(file) {\n    return new Promise((resolve, reject) => {\n        const reader = new FileReader();\n\n        reader.readAsDataURL(file);\n        reader.onloadend = (e) => {\n            if (reader.error) {\n                return reject(reader.error);\n            }\n            resolve(e.target.result);\n        };\n    });\n}\n\n// @phoenix @todo: move to Odoo plugin?\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(\n        node,\n        '[data-oe-model]:not([data-oe-field=\"arch\"]):not([data-oe-type=\"html\"]),[data-oe-translation-id]'\n    );\n}\n", "import { isProtected } from \"@html_editor/utils/dom_info\";\nimport { Plugin } from \"../plugin\";\nimport { descendants } from \"../utils/dom_traversal\";\n\nexport class CommentPlugin extends Plugin {\n    static id = \"comment\";\n    resources = {\n        normalize_handlers: this.removeComment.bind(this),\n    };\n\n    removeComment(node) {\n        for (const el of [node, ...descendants(node)]) {\n            if (el.nodeType === Node.COMMENT_NODE && !isProtected(el)) {\n                el.remove();\n                return;\n            }\n        }\n    }\n}\n", "import { Plugin } from \"../plugin\";\nimport { closestBlock, isBlock } from \"../utils/blocks\";\nimport {\n    isAllowedContent,\n    isButton,\n    isContentEditable,\n    isEditorTab,\n    isEmpty,\n    isInPre,\n    isMediaElement,\n    isProtected,\n    isSelfClosingElement,\n    isShrunkBlock,\n    isTangible,\n    isTextNode,\n    isVisibleTextNode,\n    isWhitespace,\n    isZwnbsp,\n    isZWS,\n    nextLeaf,\n    previousLeaf,\n} from \"../utils/dom_info\";\nimport { getState, isFakeLineBreak, observeMutations, prepareUpdate } from \"../utils/dom_state\";\nimport {\n    childNodes,\n    closestElement,\n    findUpTo,\n    descendants,\n    firstLeaf,\n    getCommonAncestor,\n    lastLeaf,\n    findFurthest,\n} from \"../utils/dom_traversal\";\nimport {\n    DIRECTIONS,\n    childNodeIndex,\n    endPos,\n    leftPos,\n    nodeSize,\n    rightPos,\n    startPos,\n} from \"../utils/position\";\nimport { CTYPES } from \"../utils/content_types\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { compareListTypes } from \"@html_editor/main/list/utils\";\nimport { hasTouch, isBrowserChrome } from \"@web/core/browser/feature_detection\";\n\n/**\n * @typedef {Object} RangeLike\n * @property {Node} startContainer\n * @property {number} startOffset\n * @property {Node} endContainer\n * @property {number} endOffset\n */\n\n/** @typedef {import(\"@html_editor/core/selection_plugin\").EditorSelection} EditorSelection */\n\n/**\n * @typedef {Object} DeleteShared\n * @property { DeletePlugin['delete'] } delete\n * @property { DeletePlugin['deleteRange'] } deleteRange\n * @property { DeletePlugin['deleteSelection'] } deleteSelection\n */\n\nexport class DeletePlugin extends Plugin {\n    static dependencies = [\"baseContainer\", \"selection\", \"history\", \"input\"];\n    static id = \"delete\";\n    static shared = [\"deleteRange\", \"deleteSelection\", \"delete\"];\n    resources = {\n        user_commands: [\n            { id: \"deleteBackward\", run: () => this.delete(\"backward\", \"character\") },\n            { id: \"deleteForward\", run: () => this.delete(\"forward\", \"character\") },\n            { id: \"deleteBackwardWord\", run: () => this.delete(\"backward\", \"word\") },\n            { id: \"deleteForwardWord\", run: () => this.delete(\"forward\", \"word\") },\n            { id: \"deleteBackwardLine\", run: () => this.delete(\"backward\", \"line\") },\n            { id: \"deleteForwardLine\", run: () => this.delete(\"forward\", \"line\") },\n        ],\n        shortcuts: [\n            { hotkey: \"backspace\", commandId: \"deleteBackward\" },\n            { hotkey: \"delete\", commandId: \"deleteForward\" },\n            { hotkey: \"control+backspace\", commandId: \"deleteBackwardWord\" },\n            { hotkey: \"control+delete\", commandId: \"deleteForwardWord\" },\n            { hotkey: \"control+shift+backspace\", commandId: \"deleteBackwardLine\" },\n            { hotkey: \"control+shift+delete\", commandId: \"deleteForwardLine\" },\n        ],\n        /** Handlers */\n        beforeinput_handlers: [\n            withSequence(5, this.onBeforeInputInsertText.bind(this)),\n            this.onBeforeInputDelete.bind(this),\n        ],\n        input_handlers: (ev) => this.onAndroidChromeInput?.(ev),\n        selectionchange_handlers: withSequence(5, () => this.onAndroidChromeSelectionChange?.()),\n        /** Overrides */\n        delete_backward_overrides: withSequence(30, this.deleteBackwardUnmergeable.bind(this)),\n        delete_backward_word_overrides: withSequence(20, this.deleteBackwardUnmergeable.bind(this)),\n        delete_backward_line_overrides: this.deleteBackwardUnmergeable.bind(this),\n        delete_forward_overrides: withSequence(20, this.deleteForwardUnmergeable.bind(this)),\n        delete_forward_word_overrides: this.deleteForwardUnmergeable.bind(this),\n        delete_forward_line_overrides: this.deleteForwardUnmergeable.bind(this),\n\n        // @todo @phoenix: move these predicates to different plugins\n        unremovable_node_predicates: [\n            (node) => node.classList?.contains(\"oe_unremovable\"),\n            // Monetary field\n            (node) => node.matches?.(\"[data-oe-type='monetary'] > span\"),\n        ],\n        invalid_for_base_container_predicates: (node) => this.isUnremovable(node, this.editable),\n    };\n\n    setup() {\n        this.findPreviousPosition = this.makeFindPositionFn(\"backward\");\n        this.findNextPosition = this.makeFindPositionFn(\"forward\");\n    }\n\n    // --------------------------------------------------------------------------\n    // commands\n    // --------------------------------------------------------------------------\n\n    /**\n     * @param {EditorSelection} [selection]\n     */\n    deleteSelection(selection = this.dependencies.selection.getEditableSelection()) {\n        // @todo @phoenix: handle non-collapsed selection around a ZWS\n        // see collapseIfZWS\n\n        // Normalize selection\n        selection = this.dependencies.selection.setSelection(selection);\n\n        if (selection.isCollapsed) {\n            return;\n        }\n\n        let range = this.adjustRange(selection, [\n            this.expandRangeToIncludeNonEditables,\n            this.includeEndOrStartBlock,\n            this.fullyIncludeLinks,\n        ]);\n\n        if (this.delegateTo(\"delete_range_overrides\", range)) {\n            return;\n        }\n\n        range = this.deleteRange(range);\n        this.setCursorFromRange(range);\n    }\n\n    /**\n     * @param {\"backward\"|\"forward\"} direction\n     * @param {\"character\"|\"word\"|\"line\"} granularity\n     */\n    delete(direction, granularity) {\n        const selection = this.dependencies.selection.getEditableSelection();\n\n        if (!selection.isCollapsed) {\n            this.deleteSelection(selection);\n        } else if (direction === \"backward\") {\n            this.deleteBackward(selection, granularity);\n        } else if (direction === \"forward\") {\n            this.deleteForward(selection, granularity);\n        } else {\n            throw new Error(\"Invalid direction\");\n        }\n        this.dispatchTo(\"delete_handlers\");\n        this.dependencies.history.addStep();\n    }\n\n    // --------------------------------------------------------------------------\n    // Delete backward/forward\n    // --------------------------------------------------------------------------\n\n    /**\n     * @param {EditorSelection} selection\n     * @param {\"character\"|\"word\"|\"line\"} granularity\n     */\n    deleteBackward(selection, granularity) {\n        // Normalize selection\n        const { endContainer, endOffset } = this.dependencies.selection.setSelection(selection);\n\n        let range = this.getRangeForDelete(endContainer, endOffset, \"backward\", granularity);\n\n        const resourceIds = {\n            character: \"delete_backward_overrides\",\n            word: \"delete_backward_word_overrides\",\n            line: \"delete_backward_line_overrides\",\n        };\n        if (this.delegateTo(resourceIds[granularity], range)) {\n            return;\n        }\n\n        range = this.adjustRange(range, [\n            this.includeEmptyInlineEnd,\n            this.includePreviousZWS,\n            this.includeEndOrStartBlock,\n        ]);\n        range = this.deleteRange(range);\n        this.document.getSelection()?.removeAllRanges();\n        this.setCursorFromRange(range, { collapseToEnd: true });\n    }\n\n    /**\n     * @param {EditorSelection} selection\n     * @param {\"character\"|\"word\"|\"line\"} granularity\n     */\n    deleteForward(selection, granularity) {\n        // Normalize selection\n        const { startContainer, startOffset } = this.dependencies.selection.setSelection(selection);\n\n        let range = this.getRangeForDelete(startContainer, startOffset, \"forward\", granularity);\n\n        const resourceIds = {\n            character: \"delete_forward_overrides\",\n            word: \"delete_forward_word_overrides\",\n            line: \"delete_forward_line_overrides\",\n        };\n        if (this.delegateTo(resourceIds[granularity], range)) {\n            return;\n        }\n\n        range = this.adjustRange(range, [\n            this.includeEmptyInlineStart,\n            this.includeNextZWS,\n            this.includeEndOrStartBlock,\n        ]);\n        range = this.deleteRange(range);\n        this.setCursorFromRange(range);\n    }\n\n    getRangeForDelete(node, offset, direction, granularity) {\n        let destContainer, destOffset;\n        switch (granularity) {\n            case \"character\":\n                [destContainer, destOffset] = this.findAdjacentPosition(node, offset, direction);\n                break;\n            case \"word\":\n                ({ focusNode: destContainer, focusOffset: destOffset } =\n                    this.dependencies.selection.modifySelection(\"extend\", direction, \"word\"));\n                break;\n            case \"line\":\n                [destContainer, destOffset] = this.findLineBoundary(node, offset, direction);\n                break;\n            default:\n                throw new Error(\"Invalid granularity\");\n        }\n\n        if (!destContainer) {\n            [destContainer, destOffset] = [node, offset];\n        }\n        const [startContainer, startOffset, endContainer, endOffset] =\n            direction === \"forward\"\n                ? [node, offset, destContainer, destOffset]\n                : [destContainer, destOffset, node, offset];\n\n        return { startContainer, startOffset, endContainer, endOffset };\n    }\n\n    // --------------------------------------------------------------------------\n    // Delete range\n    // --------------------------------------------------------------------------\n\n    /*\n    Inline:\n        Empty inlines get filled, no joining.\n        <b>[abc]</b> -> <b>[]ZWS</b>\n        <b>[abc</b> <b>d]ef</b> -> <b>[]ZWS</b> <b>ef</b>\n        <b>[abc</b> <b>def]</b> -> <b>[]ZWS</b> <b>ZWS</b>\n        \n    Block:\n        Shrunk blocks get filled.\n        <p>[abc]</p> -> <p>[]<br></p>\n\n        End block's content is appended to start block on join.\n        <h1>a[bc</h1> <p>de]f</p> -> <h1>a[]f</h1>\n        <h1>[abc</h1> <p>def]</p> -> <h1>[]<br></h1>\n\n        To make left block disappear instead, use this range:\n        [<h1>abc</h1> <p>de]f</p> -> []<p>f</p> (which can be normalized later, see setCursorFromRange)\n\n    Block + Inline:\n        Inline content after block is appended to block on join.\n        <p>a[bc</p> d]ef -> <p>a[]ef</p>\n\n    Inline + Block:\n        Block content is unwrapped on join.\n        ab[c <p>de]f</p> -> ab[]f\n        ab[c <p>de]f</p> ghi -> ab[]f<br>ghi\n\n    */\n\n    /**\n     * Removes (removable) nodes and merges block with block/inline when\n     * applicable (and mergeable).\n     * Returns the updated range, which is collapsed to start if the original\n     * range could be completely deleted and merged.\n     *\n     * @param {RangeLike} range\n     * @returns {RangeLike}\n     */\n    deleteRange(range) {\n        // Do nothing if the range is collapsed.\n        if (range.startContainer === range.endContainer && range.startOffset === range.endOffset) {\n            return range;\n        }\n        // Split text nodes in order to have elements as start/end containers.\n        range = this.splitTextNodes(range);\n\n        const { startContainer, startOffset, endContainer, endOffset } = range;\n        const restoreSpaces = prepareUpdate(startContainer, startOffset, endContainer, endOffset);\n\n        let restoreFakeBRs;\n        ({ restoreFakeBRs, range } = this.removeFakeBRs(range));\n\n        // Remove nodes.\n        let allNodesRemoved;\n        ({ allNodesRemoved, range } = this.removeNodes(range));\n\n        this.fillEmptyInlines(range);\n\n        // Join fragments.\n        const originalCommonAncestor = range.commonAncestorContainer;\n        if (allNodesRemoved) {\n            range = this.joinFragments(range);\n        }\n\n        restoreFakeBRs();\n        this.fillShrunkBlocks(originalCommonAncestor);\n        restoreSpaces();\n\n        return range;\n    }\n\n    splitTextNodes({ startContainer, startOffset, endContainer, endOffset }) {\n        // Splits text nodes only if necessary.\n        const split = (textNode, offset) => {\n            let didSplit = false;\n            if (offset === 0) {\n                offset = childNodeIndex(textNode);\n            } else if (offset === nodeSize(textNode)) {\n                offset = childNodeIndex(textNode) + 1;\n            } else {\n                textNode.splitText(offset);\n                didSplit = true;\n                offset = childNodeIndex(textNode) + 1;\n            }\n            return [textNode.parentElement, offset, didSplit];\n        };\n\n        if (endContainer.nodeType === Node.TEXT_NODE) {\n            [endContainer, endOffset] = split(endContainer, endOffset);\n        }\n        if (startContainer.nodeType === Node.TEXT_NODE) {\n            let didSplit;\n            [startContainer, startOffset, didSplit] = split(startContainer, startOffset);\n            if (startContainer === endContainer && didSplit) {\n                endOffset += 1;\n            }\n        }\n\n        return {\n            startContainer,\n            startOffset,\n            endContainer,\n            endOffset,\n            commonAncestorContainer: getCommonAncestor(\n                [startContainer, endContainer],\n                this.editable\n            ),\n        };\n    }\n\n    // Removes fake line breaks, so that each BR left is an actual line break.\n    // Returns the updated range and a function to later restore the fake BRs.\n    removeFakeBRs(range) {\n        let { startContainer, startOffset, endContainer, endOffset, commonAncestorContainer } =\n            range;\n        const visitedNodes = new Set();\n        const removeBRs = (container, offset) => {\n            let node = container;\n            while (node !== commonAncestorContainer) {\n                const lastBR = childNodes(node).findLast((child) => child.nodeName === \"BR\");\n                if (lastBR && isFakeLineBreak(lastBR)) {\n                    if (lastBR === container) {\n                        [container, offset] = leftPos(lastBR);\n                    } else if (node === container && offset > childNodeIndex(lastBR)) {\n                        offset -= 1;\n                    }\n                    lastBR.remove();\n                }\n                visitedNodes.add(node);\n                node = node.parentNode;\n            }\n            return [container, offset];\n        };\n        [startContainer, startOffset] = removeBRs(startContainer, startOffset);\n        [endContainer, endOffset] = removeBRs(endContainer, endOffset);\n        range = { startContainer, startOffset, endContainer, endOffset, commonAncestorContainer };\n\n        const restoreFakeBRs = () => {\n            for (const node of visitedNodes) {\n                if (!node.isConnected) {\n                    continue;\n                }\n                const lastBR = childNodes(node).findLast((child) => child.nodeName === \"BR\");\n                if (lastBR && isFakeLineBreak(lastBR)) {\n                    lastBR.after(this.document.createElement(\"br\"));\n                }\n                // Shrunk blocks are restored by `fillShrunkBlocks`.\n            }\n        };\n\n        return { restoreFakeBRs, range };\n    }\n\n    fillEmptyInlines(range) {\n        const nodes = [range.startContainer];\n        if (range.endContainer !== range.startContainer) {\n            nodes.push(range.endContainer);\n        }\n        for (const node of nodes) {\n            // @todo: mind Icons?\n            // Probably need to get deepest position's element\n            // @todo: update fillEmpty\n            // @todo: check if nodes does not already have a ZWS/ZWNBSP\n            if (!isBlock(node) && !isTangible(node)) {\n                node.appendChild(this.document.createTextNode(\"\\u200B\"));\n                node.setAttribute(\"data-oe-zws-empty-inline\", \"\");\n            }\n        }\n    }\n\n    fillShrunkBlocks(commonAncestor) {\n        const fillBlock = (block) => {\n            if (\n                block.matches(\"div[contenteditable='true']\") &&\n                !block.parentElement.isContentEditable\n            ) {\n                // @todo: not sure we want this when allowInlineAtRoot is true\n                const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n                baseContainer.appendChild(this.document.createElement(\"br\"));\n                block.appendChild(baseContainer);\n            } else {\n                block.appendChild(this.document.createElement(\"br\"));\n            }\n        };\n        // @todo: this ends up filling shrunk blocks outside the affected range.\n        // Ideally, it should only affect the block within the boundaries of the\n        // original range.\n        for (const node of descendants(commonAncestor).reverse()) {\n            if (isBlock(node) && isShrunkBlock(node)) {\n                fillBlock(node);\n            }\n        }\n        const containingBlock = closestBlock(commonAncestor);\n        if (isShrunkBlock(containingBlock)) {\n            fillBlock(containingBlock);\n        }\n    }\n\n    // --------------------------------------------------------------------------\n    // Remove nodes\n    // --------------------------------------------------------------------------\n\n    removeNodes(range) {\n        const { startContainer, startOffset, endContainer, commonAncestorContainer } = range;\n        let { endOffset } = range;\n        const nodesToRemove = [];\n\n        // Pick child nodes to the right for later removal, propagate until\n        // commonAncestorContainer (non-inclusive)\n        let node = startContainer;\n        let startRemoveIndex = startOffset;\n        while (node !== commonAncestorContainer) {\n            for (let i = startRemoveIndex; i < node.childNodes.length; i++) {\n                nodesToRemove.push(node.childNodes[i]);\n            }\n            startRemoveIndex = childNodeIndex(node) + 1;\n            node = node.parentElement;\n        }\n\n        // Pick child nodes to the left for later removal, propagate until\n        // commonAncestorContainer (non-inclusive)\n        node = endContainer;\n        let endRemoveIndex = endOffset;\n        while (node !== commonAncestorContainer) {\n            for (let i = 0; i < endRemoveIndex; i++) {\n                nodesToRemove.push(node.childNodes[i]);\n            }\n            endRemoveIndex = childNodeIndex(node);\n            node = node.parentElement;\n        }\n\n        // Pick commonAncestorContainer's direct children for removal\n        for (let i = startRemoveIndex; i < endRemoveIndex; i++) {\n            nodesToRemove.push(commonAncestorContainer.childNodes[i]);\n        }\n\n        // Remove nodes\n        let allNodesRemoved = true;\n        for (const node of nodesToRemove) {\n            const parent = node.parentNode;\n            const didRemove = this.removeNode(node);\n            allNodesRemoved &&= didRemove;\n            if (didRemove && endContainer === parent) {\n                endOffset -= 1;\n            }\n        }\n\n        const endContainerList = closestElement(endContainer, \"UL, OL\");\n        if (\n            [\"OL\", \"UL\"].includes(startContainer.nodeName) &&\n            endContainerList &&\n            !compareListTypes(startContainer, endContainerList)\n        ) {\n            const newRange = this.document.createRange();\n            newRange.setStart(range.endContainer, endOffset);\n            return { allNodesRemoved, range: newRange };\n        }\n        return { allNodesRemoved, range: { ...range, endOffset } };\n    }\n\n    // The root argument is used by some predicates in which a node is\n    // conditionally unremovable (e.g. a table cell is only removable if its\n    // ancestor table is also being removed).\n    isUnremovable(node, root = undefined) {\n        return this.getResource(\"unremovable_node_predicates\").some((p) => p(node, root));\n    }\n\n    // Returns true if the entire subtree rooted at node was removed.\n    // Unremovable nodes take the place of removable ancestors.\n    removeNode(node) {\n        const root = node;\n        const remove = (node) => {\n            let customHandling = false;\n            let customIsUnremovable;\n            for (const cb of this.getResource(\"removable_descendants_providers\")) {\n                const descendantsToRemove = cb(node);\n                if (descendantsToRemove) {\n                    for (const descendant of descendantsToRemove) {\n                        remove(descendant);\n                    }\n                    customHandling = true;\n                    customIsUnremovable = this.isUnremovable(node, root);\n                    if (!customIsUnremovable) {\n                        // TODO ABD: test protected + unremovable\n                        node.remove();\n                    }\n                }\n            }\n            if (customHandling) {\n                return !customIsUnremovable;\n            }\n            for (const child of [...node.childNodes]) {\n                remove(child);\n            }\n            if (this.isUnremovable(node, root)) {\n                return false;\n            }\n            if (node.hasChildNodes()) {\n                node.before(...node.childNodes);\n                node.remove();\n                return false;\n            }\n            node.remove();\n            return true;\n        };\n        return remove(node);\n    }\n\n    // --------------------------------------------------------------------------\n    // Join\n    // --------------------------------------------------------------------------\n\n    // Joins both ends of the range if possible: block + block/inline.\n    // If joined, the range is collapsed to start.\n    // Returns the updated range.\n    joinFragments(range) {\n        const joinableLeft = this.getJoinableFragment(range, \"start\");\n        const joinableRight = this.getJoinableFragment(range, \"end\");\n        const join = this.getJoinOperation(joinableLeft.type, joinableRight.type);\n\n        const didJoin = join(joinableLeft.node, joinableRight.node, range.commonAncestorContainer);\n\n        return didJoin ? this.collapseRange(range) : range;\n    }\n\n    /**\n     * Retrieves the joinable fragment based on the given range and side.\n     *\n     * @param {Object} range - range-like object.\n     * @param {\"start\"|\"end\"} side\n     * @returns {Object} - { node: Node|null, type: \"block\"|\"inline\"|\"null\" }\n     */\n    getJoinableFragment(range, side) {\n        const commonAncestor = range.commonAncestorContainer;\n        const container = side === \"start\" ? range.startContainer : range.endContainer;\n        const offset = side === \"start\" ? range.startOffset : range.endOffset;\n\n        if (container === range.commonAncestorContainer) {\n            // This means a direct child of the commonAncestor was removed.\n            // The joinable in this case is its sibling (previous for the start\n            // side, next for the end side), but only if inline.\n            const sibling = childNodes(commonAncestor)[side === \"start\" ? offset - 1 : offset];\n            if (sibling && !isBlock(sibling) && !(sibling.nodeType === Node.TEXT_NODE && !isVisibleTextNode(sibling))) {\n                return { node: sibling, type: \"inline\" };\n            }\n            // No fragment to join.\n            return { node: null, type: \"null\" };\n        }\n        // Starting from `container`, find the closest block up to\n        // (not-inclusive) the common ancestor. If not found, keep the common\n        // ancestor's child inline element.\n        let last;\n        let element = container;\n        while (element !== commonAncestor) {\n            if (isBlock(element)) {\n                return { node: element, type: \"block\" };\n            }\n            last = element;\n            element = element.parentElement;\n        }\n        return { node: last, type: \"inline\" };\n    }\n\n    getJoinOperation(leftType, rightType) {\n        return (\n            {\n                \"block + block\": this.joinBlocks,\n                \"block + inline\": this.joinInlineIntoBlock,\n                \"inline + block\": this.joinBlockIntoInline,\n            }[leftType + \" + \" + rightType] || (() => true)\n        ).bind(this);\n        // \"inline + inline\": Nothing to do, consider it joined.\n        // Same any combination involving type \"null\" (no joinable element).\n    }\n\n    /**\n     * An unsplittable element is also unmergeable and vice-versa (as split and\n     * merge are reverse operations from one another).\n     */\n    isUnmergeable(node) {\n        return this.getResource(\"unsplittable_node_predicates\").some((p) => p(node));\n    }\n\n    joinBlocks(left, right, commonAncestor) {\n        // Check if both blocks are mergeable.\n        const canMerge = (n) => !findUpTo(n, commonAncestor, this.isUnmergeable.bind(this));\n        if (!canMerge(left) || !canMerge(right)) {\n            return false;\n        }\n\n        // Check if left block allows right block's content.\n        const rightChildNodes = childNodes(right);\n        if (!isAllowedContent(left, rightChildNodes)) {\n            return false;\n        }\n\n        left.append(...rightChildNodes);\n        let toRemove = right;\n        let parent = right.parentElement;\n        // Propagate until commonAncestor, removing empty blocks\n        while (parent !== commonAncestor && parent.childNodes.length === 1) {\n            toRemove = parent;\n            parent = parent.parentElement;\n        }\n        toRemove.remove();\n        return true;\n    }\n\n    joinInlineIntoBlock(leftBlock, rightInline, commonAncestor) {\n        if (findUpTo(leftBlock, commonAncestor, (node) => this.isUnmergeable(node))) {\n            // Left block is unmergeable.\n            return false;\n        }\n\n        // @todo: avoid appending a BR as last child of the block\n        while (rightInline && !isBlock(rightInline)) {\n            const toAppend = rightInline;\n            rightInline = rightInline.nextSibling;\n            leftBlock.append(toAppend);\n        }\n        return true;\n    }\n\n    joinBlockIntoInline(leftInline, rightBlock, commonAncestor) {\n        if (findUpTo(rightBlock, commonAncestor, (node) => this.isUnmergeable(node))) {\n            // Right block is unmergeable.\n            return false;\n        }\n\n        leftInline.after(...childNodes(rightBlock));\n        let toRemove = rightBlock;\n        let parent = rightBlock.parentElement;\n        // Propagate until commonAncestor, removing empty blocks\n        while (parent !== commonAncestor && parent.childNodes.length === 1) {\n            toRemove = parent;\n            parent = parent.parentElement;\n        }\n        // Restore line break between removed block and inline content after it.\n        if (parent === commonAncestor) {\n            const rightSibling = toRemove.nextSibling;\n            if (rightSibling && !isBlock(rightSibling)) {\n                rightSibling.before(this.document.createElement(\"br\"));\n            }\n        }\n        toRemove.remove();\n        return true;\n    }\n\n    // --------------------------------------------------------------------------\n    // Adjust range\n    // --------------------------------------------------------------------------\n\n    /**\n     * @param {RangeLike}\n     * @param {((range: Range) => Range)[]} callbacks\n     * @returns {RangeLike}\n     */\n    adjustRange({ startContainer, startOffset, endContainer, endOffset }, callbacks) {\n        let range = this.document.createRange();\n        range.setStart(startContainer, startOffset);\n        range.setEnd(endContainer, endOffset);\n\n        for (const callback of callbacks) {\n            range = callback.call(this, range);\n        }\n\n        ({ startContainer, startOffset, endOffset, endContainer } = range);\n        return { startContainer, startOffset, endOffset, endContainer };\n    }\n\n    /**\n     * <h1>[abc</h1><p>d]ef</p> -> [<h1>abc</h1><p>d]ef</p>\n     *\n     * @param {HTMLElement} block\n     * @param {Range} range\n     * @returns {Range}\n     */\n    includeBlockStart(block, range) {\n        const { startContainer, startOffset, commonAncestorContainer } = range;\n        if (\n            block === commonAncestorContainer ||\n            !this.isCursorAtStartOfElement(block, startContainer, startOffset)\n        ) {\n            return range;\n        }\n        range.setStartBefore(block);\n        return this.includeBlockStart(block.parentNode, range);\n    }\n\n    /**\n     * <p>ab[c</p><div>def]</div> ->  <p>ab[c</p><div>def</div>]\n     *\n     * @param {HTMLElement} block\n     * @param {Range} range\n     * @returns {Range}\n     */\n    includeBlockEnd(block, range) {\n        const { startContainer, endContainer, endOffset, commonAncestorContainer } = range;\n        const startList = closestElement(startContainer, \"UL, OL\");\n        const endList = closestElement(endContainer, \"UL, OL\");\n        if (\n            block === commonAncestorContainer ||\n            !this.isCursorAtEndOfElement(block, endContainer, endOffset) ||\n            (startList && endList && !compareListTypes(startList, endList))\n        ) {\n            return range;\n        }\n        range.setEndAfter(block);\n        return this.includeBlockEnd(block.parentNode, range);\n    }\n\n    /**\n     * If range spans two blocks, try to fully include the right (end) one OR\n     * the left (start) one (but not both).\n     *\n     * E.g.:\n     * Fully includes the right block:\n     * <p>ab[c</p><div>def]</div> ->  <p>ab[c</p><div>def</div>]\n     * <p>[abc</p><div>def]</div> ->  <p>[abc</p><div>def</div>]\n     *\n     * Fully includes the left block:\n     * <h1>[abc</h1><p>d]ef</p> -> [<h1>abc</h1><p>d]ef</p>\n     *\n     * @param {Range} range\n     * @returns {Range}\n     */\n    includeEndOrStartBlock(range) {\n        const { startContainer, endContainer, commonAncestorContainer } = range;\n        const startBlock = findUpTo(startContainer, commonAncestorContainer, isBlock);\n        const endBlock = findUpTo(endContainer, commonAncestorContainer, isBlock);\n        if (!startBlock || !endBlock) {\n            return range;\n        }\n        range = this.includeBlockEnd(endBlock, range);\n        // Only include start block if end block could not be included.\n        if (range.endContainer === endContainer) {\n            range = this.includeBlockStart(startBlock, range);\n        }\n        return range;\n    }\n\n    /**\n     * Fully select link if:\n     * - range spans content inside and outside the link AND\n     * - all of its content is selected.\n     *\n     * <a>[abc</a>d]ef -> [<a>abc</a>d]ef\n     * ab[c<a>def]</a> ->  ab[c<a>def</a>]\n     * But:\n     * <a>[abc]</a> -> <a>[abc]</a> (remains unchanged)\n     *\n     * @param {Range} range\n     * @returns {Range}\n     */\n    fullyIncludeLinks(range) {\n        const { startContainer, startOffset, endContainer, endOffset, commonAncestorContainer } =\n            range;\n        const [startLink, endLink] = [startContainer, endContainer].map((container) =>\n            findUpTo(container, commonAncestorContainer, (node) => node.nodeName === \"A\")\n        );\n        if (startLink && this.isCursorAtStartOfElement(startLink, startContainer, startOffset)) {\n            range.setStartBefore(startLink);\n        }\n        if (endLink && this.isCursorAtEndOfElement(endLink, endContainer, endOffset)) {\n            range.setEndAfter(endLink);\n        }\n        return range;\n    }\n\n    /**\n     * @param {Range} range\n     * @returns {Range}\n     */\n    includeEmptyInlineStart(range) {\n        const element = closestElement(range.startContainer);\n        if (this.isEmptyInline(element)) {\n            range.setStartBefore(element);\n        }\n        return range;\n    }\n\n    /**\n     * @param {Range} range\n     * @returns {Range}\n     */\n    includeEmptyInlineEnd(range) {\n        const element = closestElement(range.endContainer);\n        if (this.isEmptyInline(element)) {\n            range.setEndAfter(element);\n        }\n        return range;\n    }\n\n    // @todo @phoenix This is here because of the second test case in\n    // delete/forward/selection collapsed/basic/should ignore ZWS, and its\n    // importance is questionable.\n    /**\n     * @param {Range} range\n     * @returns {Range}\n     */\n    includeNextZWS(range) {\n        const { endContainer, endOffset } = range;\n        if (isTextNode(endContainer) && endContainer.textContent[endOffset] === \"\\u200B\") {\n            range.setEnd(endContainer, endOffset + 1);\n        }\n        return range;\n    }\n\n    /**\n     * @param {Range} range\n     * @returns {Range}\n     */\n    includePreviousZWS(range) {\n        const { startContainer, startOffset } = range;\n        if (\n            isTextNode(startContainer) &&\n            startContainer.textContent[startOffset - 1] === \"\\u200B\"\n        ) {\n            range.setStart(startContainer, startOffset - 1);\n        }\n        return range;\n    }\n\n    // Expand the range to fully include all contentEditable=False elements.\n    /**\n     * @param {Range} range\n     * @returns {Range}\n     */\n    expandRangeToIncludeNonEditables(range) {\n        const { startContainer, endContainer, commonAncestorContainer: commonAncestor } = range;\n        const isNonEditable = (node) => !isContentEditable(node);\n        const startUneditable = findFurthest(startContainer, commonAncestor, isNonEditable);\n        if (startUneditable) {\n            // @todo @phoenix: Review this spec. I suggest this instead (no block merge after removing):\n            // startContainer = startUneditable.parentElement;\n            // startOffset = childNodeIndex(startUneditable);\n            const leaf = previousLeaf(startUneditable);\n            if (leaf) {\n                range.setStart(leaf, nodeSize(leaf));\n            } else {\n                range.setStart(commonAncestor, 0);\n            }\n        }\n        const endUneditable = findFurthest(endContainer, commonAncestor, isNonEditable);\n        if (endUneditable) {\n            range.setEndAfter(endUneditable);\n        }\n        return range;\n    }\n\n    // --------------------------------------------------------------------------\n    // Find previous/next position\n    // --------------------------------------------------------------------------\n\n    /**\n     * Returns the next/previous position for deletion.\n     *\n     * @param {Node} node\n     * @param {number} offset\n     * @param {\"forward\"|\"backward\"} direction\n     * @returns {[Node|null, Number|null]}\n     */\n    findAdjacentPosition(node, offset, direction) {\n        return direction === \"forward\"\n            ? this.findNextPosition(node, offset)\n            : this.findPreviousPosition(node, offset);\n    }\n\n    /**\n     *  Returns a function to find the adjacent position in the given direction.\n     *\n     * @param {\"forward\"|\"backward\"} direction\n     */\n    makeFindPositionFn(direction) {\n        const isDirectionForward = direction === \"forward\";\n\n        // Define helper functions based on the direction.\n        // Text node helpers.\n        const findVisibleChar = (\n            isDirectionForward ? this.findNextVisibleChar : this.findPreviousVisibleChar\n        ).bind(this);\n        const charLeftPos = (index, char) => index;\n        const charRightPos = (index, char) => index + char.length;\n        const indexBeforeChar = isDirectionForward ? charLeftPos : charRightPos;\n        const indexAfterChar = isDirectionForward ? charRightPos : charLeftPos;\n        const textEdgePos = isDirectionForward ? startPos : endPos;\n        // Leaf helpers.\n        const adjacentLeaf = (isDirectionForward ? this.nextLeaf : this.previousLeaf).bind(this);\n        const adjacentLeafFromPos = (\n            isDirectionForward ? this.nextLeafFromPos : this.previousLeafFromPos\n        ).bind(this);\n        const beforePos = isDirectionForward ? leftPos : rightPos;\n        const afterPos = isDirectionForward ? rightPos : leftPos;\n\n        /**\n         * Returns the next/previous position for deletion.\n         *\n         * \"Before\" and \"after\" have different meanings depending on the\n         * direction: before and after mean, respectively, previous and next in\n         * DOM order when direction is \"forward\", and the other way around when\n         * direction is \"backward\".\n         *\n         * @param {Node} node\n         * @param {number} offset\n         * @returns {[Node|null, Number|null]}\n         */\n        return function findPosition(node, offset) {\n            if (node.nodeType === Node.TEXT_NODE) {\n                const [char, index] = findVisibleChar(node, offset);\n                if (char) {\n                    return [node, indexAfterChar(index, char)];\n                }\n            }\n\n            // Define context: search is restricted to the closest editable root.\n            const isEditableRoot = (n) => n.isContentEditable && !n.parentNode.isContentEditable;\n            const editableRoot = findUpTo(node, this.editable.parentNode, isEditableRoot);\n\n            let blockSwitch;\n            const nodeClosestBlock = closestBlock(node);\n            let leaf = adjacentLeafFromPos(node, offset, editableRoot);\n            while (leaf) {\n                blockSwitch ||= closestBlock(leaf) !== nodeClosestBlock;\n\n                if (this.shouldSkip(leaf, blockSwitch)) {\n                    leaf = adjacentLeaf(leaf, editableRoot);\n                    continue;\n                }\n\n                if (leaf.nodeType === Node.TEXT_NODE) {\n                    const [char, index] = findVisibleChar(...textEdgePos(leaf));\n                    if (char) {\n                        const idx = (blockSwitch ? indexBeforeChar : indexAfterChar)(index, char);\n                        return [leaf, idx];\n                    }\n                } else if (!leaf.isContentEditable && isBlock(leaf)) {\n                    // E.g. Desired range for deleteForward:\n                    // <p>abc[</p><div contenteditable=\"false\">def</div>]<p>ghi</p>\n                    return afterPos(leaf);\n                } else {\n                    return blockSwitch ? beforePos(leaf) : afterPos(leaf);\n                }\n                leaf = adjacentLeaf(leaf, editableRoot);\n            }\n            return [null, null];\n        };\n    }\n\n    findLineBoundary(container, offset, direction) {\n        const adjacentLeaf = direction === \"forward\" ? nextLeaf : previousLeaf;\n        const edgeIndex = (node) => (direction === \"forward\" ? nodeSize(node) : 0);\n        const block = closestBlock(container);\n        let last = container;\n        let node = adjacentLeaf(container, this.editable);\n        // look for a BR or a block start\n        while (node && node.nodeName !== \"BR\" && closestBlock(node) === block) {\n            last = node;\n            node = adjacentLeaf(node, this.editable);\n        }\n        if (last === container && offset === edgeIndex(container)) {\n            // Cursor is already next to the line break, go to following position.\n            return this.findAdjacentPosition(container, offset, direction);\n        }\n        return direction === \"forward\" ? rightPos(last) : leftPos(last);\n    }\n\n    // @todo @phoenix: there are not enough tests for visibility of characters\n    // (invisible whitespace, separate nodes, etc.)\n    isVisibleChar(char, textNode, offset) {\n        // Protected nodes are always \"visible\" for the editor\n        if (isProtected(textNode)) {\n            // TODO ABD: add test\n            return true;\n        }\n        const isZwnbspLinkPad = (node) =>\n            isButton(node.previousSibling) || isButton(node.nextSibling);\n        if (isZwnbsp(textNode) && isZwnbspLinkPad(textNode)) {\n            return true;\n        }\n        // ZWS and ZWNBSP are invisible.\n        if ([\"\\u200B\", \"\\uFEFF\"].includes(char)) {\n            return false;\n        }\n        if (!isWhitespace(char) || isInPre(textNode)) {\n            return true;\n        }\n\n        // Assess visibility of whitespace.\n        // Whitespace is visible if it's immediately preceded by content, and\n        // followed by content before a BR or block start/end.\n\n        // If not preceded by content, it is invisible.\n        if (offset) {\n            return !isWhitespace(textNode.textContent[offset - char.length]);\n        } else if (!(getState(...leftPos(textNode), DIRECTIONS.LEFT).cType & CTYPES.CONTENT)) {\n            return false;\n        }\n\n        // Space is only visible if it's followed by content (with an optional\n        // sequence of invisible spaces in between), before a BR or block\n        // end/start.\n        const charsToTheRight = textNode.textContent.slice(offset + char.length);\n        for (char of charsToTheRight) {\n            if (!isWhitespace(char)) {\n                return true;\n            }\n        }\n        // No content found in text node, look to the right of it\n        if (getState(...rightPos(textNode), DIRECTIONS.RIGHT).cType & CTYPES.CONTENT) {\n            return true;\n        }\n\n        return false;\n    }\n\n    shouldSkip(leaf, blockSwitch) {\n        if (leaf.nodeType === Node.TEXT_NODE) {\n            return false;\n        }\n        // @todo Maybe skip anything that is not an element (e.g. comment nodes)\n        if (blockSwitch) {\n            return false;\n        }\n        if (leaf.nodeName === \"BR\" && isFakeLineBreak(leaf)) {\n            return true;\n        }\n        // @todo: register these as resources by other plugins?\n        if (\n            [isSelfClosingElement, isMediaElement, isEditorTab].some((predicate) => predicate(leaf))\n        ) {\n            return false;\n        }\n        if (isEmpty(leaf) || isZWS(leaf)) {\n            return true;\n        }\n        return false;\n    }\n\n    findPreviousVisibleChar(textNode, index) {\n        // @todo @phoenix: write tests for chars with size > 1 (emoji, etc.)\n        // Use the string iterator to handle surrogate pairs.\n        const chars = [...textNode.textContent.slice(0, index)];\n        let char = chars.pop();\n        while (char) {\n            index -= char.length;\n            if (this.isVisibleChar(char, textNode, index)) {\n                return [char, index];\n            }\n            char = chars.pop();\n        }\n        return [null, null];\n    }\n\n    findNextVisibleChar(textNode, index) {\n        // Use the string iterator to handle surrogate pairs.\n        for (const char of textNode.textContent.slice(index)) {\n            if (this.isVisibleChar(char, textNode, index)) {\n                return [char, index];\n            }\n            index += char.length;\n        }\n        return [null, null];\n    }\n\n    // If leaf is part of a contenteditable=false tree, consider its root as the\n    // leaf instead.\n    adjustedLeaf(leaf, refEditableRoot) {\n        const isNonEditable = (node) => !isContentEditable(node);\n        const nonEditableRoot = leaf && findFurthest(leaf, refEditableRoot, isNonEditable);\n        return nonEditableRoot || leaf;\n    }\n\n    previousLeaf(node, editableRoot) {\n        return this.adjustedLeaf(previousLeaf(node, editableRoot), editableRoot);\n    }\n\n    nextLeaf(node, editableRoot) {\n        return this.adjustedLeaf(nextLeaf(node, editableRoot), editableRoot);\n    }\n\n    previousLeafFromPos(node, offset, editableRoot) {\n        const leaf =\n            node.hasChildNodes() && offset > 0\n                ? lastLeaf(node.childNodes[offset - 1])\n                : previousLeaf(node, editableRoot);\n        return this.adjustedLeaf(leaf, editableRoot);\n    }\n\n    nextLeafFromPos(node, offset, editableRoot) {\n        const leaf =\n            node.hasChildNodes() && offset < nodeSize(node)\n                ? firstLeaf(node.childNodes[offset])\n                : nextLeaf(node, editableRoot);\n        return this.adjustedLeaf(leaf, editableRoot);\n    }\n\n    // --------------------------------------------------------------------------\n    // Event handlers\n    // --------------------------------------------------------------------------\n\n    onBeforeInputDelete(ev) {\n        const handledInputTypes = {\n            deleteContentBackward: [\"backward\", \"character\"],\n            deleteContentForward: [\"forward\", \"character\"],\n            deleteWordBackward: [\"backward\", \"word\"],\n            deleteWordForward: [\"forward\", \"word\"],\n            deleteHardLineBackward: [\"backward\", \"line\"],\n            deleteHardLineForward: [\"forward\", \"line\"],\n        };\n        const argsForDelete = handledInputTypes[ev.inputType];\n        if (argsForDelete) {\n            this.delete(...argsForDelete);\n            ev.preventDefault();\n            if (isBrowserChrome() && hasTouch()) {\n                this.preventDefaultDeleteAndroidChrome(ev);\n            }\n        }\n    }\n\n    onBeforeInputInsertText(ev) {\n        if (ev.inputType === \"insertText\") {\n            const selection = this.dependencies.selection.getSelectionData().deepEditableSelection;\n            if (!selection.isCollapsed) {\n                this.deleteSelection(selection);\n            }\n            // Default behavior: insert text and trigger input event\n        }\n    }\n\n    /**\n     * Beforeinput event of type deleteContentBackward cannot be default\n     * prevented in Android Chrome. So we need to revert:\n     * - eventual mutations between beforeinput and input events\n     * - eventual selection change after input event\n     *\n     * @param {InputEvent} beforeInputEvent\n     */\n    preventDefaultDeleteAndroidChrome(beforeInputEvent) {\n        const restoreDOM = this.dependencies.history.makeSavePoint();\n        this.onAndroidChromeInput = (ev) => {\n            if (ev.inputType !== beforeInputEvent.inputType) {\n                return;\n            }\n            // Revert DOM changes that occurred between beforeinput and input.\n            restoreDOM();\n\n            // Revert selection changes after input event, within the same tick.\n            // If further mutations occurred, consider selection change legit\n            // (e.g. dictionary input) and do not revert it.\n            const { restore: restoreSelection } = this.dependencies.selection.preserveSelection();\n            const observerOptions = { childList: true, subtree: true, characterData: true };\n            const getMutationRecords = observeMutations(this.editable, observerOptions);\n            this.onAndroidChromeSelectionChange = () => {\n                const shouldRevertSelectionChanges = !getMutationRecords().length;\n                if (shouldRevertSelectionChanges) {\n                    restoreSelection();\n                }\n            };\n            setTimeout(() => delete this.onAndroidChromeSelectionChange);\n        };\n    }\n\n    // ======== AD-HOC STUFF ========\n\n    deleteBackwardUnmergeable(range) {\n        const { startContainer, startOffset, endContainer, endOffset } = range;\n        return this.deleteCharUnmergeable(endContainer, endOffset, startContainer, startOffset);\n    }\n\n    // @todo @phoenix: write tests for this\n    deleteForwardUnmergeable(range) {\n        const { startContainer, startOffset, endContainer, endOffset } = range;\n        return this.deleteCharUnmergeable(startContainer, startOffset, endContainer, endOffset);\n    }\n\n    // Trap cursor inside unmergeable element. Remove it if empty.\n    deleteCharUnmergeable(sourceContainer, sourceOffset, destContainer, destOffset) {\n        if (!destContainer) {\n            return;\n        }\n        const commonAncestor = getCommonAncestor([sourceContainer, destContainer], this.editable);\n        const closestUnmergeable = findUpTo(sourceContainer, commonAncestor, (node) =>\n            this.isUnmergeable(node)\n        );\n        if (!closestUnmergeable) {\n            return;\n        }\n\n        if (\n            (isEmpty(closestUnmergeable) ||\n                this.delegateTo(\"is_empty_predicates\", closestUnmergeable)) &&\n            !this.isUnremovable(closestUnmergeable)\n        ) {\n            closestUnmergeable.remove();\n            this.dependencies.selection.setSelection({\n                anchorNode: destContainer,\n                anchorOffset: destOffset,\n            });\n        } else {\n            this.dependencies.selection.setSelection({\n                anchorNode: sourceContainer,\n                anchorOffset: sourceOffset,\n            });\n        }\n        return true;\n    }\n\n    // --------------------------------------------------------------------------\n    // utils\n    // --------------------------------------------------------------------------\n\n    isEmptyInline(element) {\n        if (isBlock(element)) {\n            return false;\n        }\n        if (isZWS(element)) {\n            return true;\n        }\n        return element.innerHTML.trim() === \"\";\n    }\n\n    isCursorAtStartOfElement(element, cursorNode, cursorOffset) {\n        const [node] = this.findPreviousPosition(cursorNode, cursorOffset);\n        return !element.contains(node);\n    }\n\n    isCursorAtEndOfElement(element, cursorNode, cursorOffset) {\n        const [node] = this.findNextPosition(cursorNode, cursorOffset);\n        return !element.contains(node);\n    }\n\n    /**\n     * @param {RangeLike} range\n     */\n    setCursorFromRange(range, { collapseToEnd = false } = {}) {\n        range = this.collapseRange(range, { toEnd: collapseToEnd });\n        const [anchorNode, anchorOffset] = this.normalizeEnterBlock(\n            range.startContainer,\n            range.startOffset\n        );\n        this.dependencies.selection.setSelection({ anchorNode, anchorOffset });\n    }\n\n    // @todo: no need for this once selection in the editable root is corrected?\n    normalizeEnterBlock(node, offset) {\n        while (isBlock(node.childNodes[offset])) {\n            [node, offset] = [node.childNodes[offset], 0];\n        }\n        return [node, offset];\n    }\n\n    /**\n     * @param {RangeLike} range\n     */\n    collapseRange(range, { toEnd = false } = {}) {\n        let { startContainer, startOffset, endContainer, endOffset } = range;\n        if (toEnd) {\n            [startContainer, startOffset] = [endContainer, endOffset];\n        } else {\n            [endContainer, endOffset] = [startContainer, startOffset];\n        }\n        const commonAncestorContainer = startContainer;\n        return { startContainer, startOffset, endContainer, endOffset, commonAncestorContainer };\n    }\n}\n", "import { Plugin } from \"../plugin\";\n\n/**\n * @typedef {typeof import(\"@odoo/owl\").Component} Component\n * @typedef {import(\"@web/core/dialog/dialog_service\").DialogServiceInterfaceAddOptions} DialogServiceInterfaceAddOptions\n */\n\n/**\n * @typedef {Object} DialogShared\n * @property {DialogPlugin['addDialog']} addDialog\n */\n\nexport class DialogPlugin extends Plugin {\n    static id = \"dialog\";\n    static dependencies = [\"selection\"];\n    static shared = [\"addDialog\"];\n\n    /**\n     * @param {Component} DialogClass\n     * @param {Object} props\n     * @param {DialogServiceInterfaceAddOptions} options\n     * @returns {Promise<void>}\n     */\n    addDialog(DialogClass, props, options = {}) {\n        return new Promise((resolve) => {\n            this.services.dialog.add(DialogClass, props, {\n                onClose: () => {\n                    this.dependencies.selection.focusEditable();\n                    resolve();\n                },\n                ...options,\n            });\n        });\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Plugin } from \"../plugin\";\nimport { closestBlock, isBlock } from \"../utils/blocks\";\nimport {\n    cleanTrailingBR,\n    fillEmpty,\n    fillShrunkPhrasingParent,\n    makeContentsInline,\n    removeClass,\n    splitTextNode,\n    unwrapContents,\n    wrapInlinesInBlocks,\n} from \"../utils/dom\";\nimport {\n    allowsParagraphRelatedElements,\n    getDeepestPosition,\n    isContentEditable,\n    isContentEditableAncestor,\n    isEmptyBlock,\n    isListElement,\n    isListItemElement,\n    isParagraphRelatedElement,\n    isProtecting,\n    isProtected,\n    isSelfClosingElement,\n    isShrunkBlock,\n    isTangible,\n    isUnprotecting,\n    listElementSelector,\n    paragraphRelatedElementsSelector,\n} from \"../utils/dom_info\";\nimport {\n    childNodes,\n    children,\n    closestElement,\n    descendants,\n    firstLeaf,\n    lastLeaf,\n} from \"../utils/dom_traversal\";\nimport { FONT_SIZE_CLASSES, TEXT_STYLE_CLASSES } from \"../utils/formatting\";\nimport { DIRECTIONS, childNodeIndex, nodeSize, rightPos } from \"../utils/position\";\nimport { normalizeCursorPosition } from \"@html_editor/utils/selection\";\nimport { baseContainerGlobalSelector } from \"@html_editor/utils/base_container\";\n\n/**\n * Get distinct connected parents of nodes\n *\n * @param {Iterable} nodes\n * @returns {Set}\n */\nfunction getConnectedParents(nodes) {\n    const parents = new Set();\n    for (const node of nodes) {\n        if (node.isConnected && node.parentElement) {\n            parents.add(node.parentElement);\n        }\n    }\n    return parents;\n}\n\n/**\n * @typedef {Object} DomShared\n * @property { DomPlugin['insert'] } insert\n * @property { DomPlugin['copyAttributes'] } copyAttributes\n */\n\nexport class DomPlugin extends Plugin {\n    static id = \"dom\";\n    static dependencies = [\"baseContainer\", \"selection\", \"history\", \"split\", \"delete\", \"lineBreak\"];\n    static shared = [\"insert\", \"copyAttributes\", \"setTag\", \"setTagName\"];\n    resources = {\n        user_commands: [\n            { id: \"insertFontAwesome\", run: this.insertFontAwesome.bind(this) },\n            { id: \"setTag\", run: this.setTag.bind(this) },\n            {\n                id: \"insertSeparator\",\n                title: _t(\"Separator\"),\n                description: _t(\"Insert a horizontal rule separator\"),\n                icon: \"fa-minus\",\n                run: this.insertSeparator.bind(this),\n            },\n        ],\n        powerbox_items: {\n            categoryId: \"structure\",\n            commandId: \"insertSeparator\",\n        },\n        /** Handlers */\n        clean_handlers: this.removeEmptyClassAndStyleAttributes.bind(this),\n        clean_for_save_handlers: ({ root }) => {\n            this.removeEmptyClassAndStyleAttributes(root);\n            for (const el of root.querySelectorAll(\"hr[contenteditable]\")) {\n                el.removeAttribute(\"contenteditable\");\n            }\n        },\n        normalize_handlers: this.normalize.bind(this),\n    };\n    contentEditableToRemove = new Set();\n\n    // Shared\n\n    /**\n     * @param {string | DocumentFragment | Element | null} content\n     */\n    insert(content) {\n        if (!content) {\n            return;\n        }\n        let selection = this.dependencies.selection.getEditableSelection();\n        let startNode;\n        let insertBefore = false;\n        if (!selection.isCollapsed) {\n            this.dependencies.delete.deleteSelection();\n            selection = this.dependencies.selection.getEditableSelection();\n        }\n        if (selection.startContainer.nodeType === Node.TEXT_NODE) {\n            insertBefore = !selection.startOffset;\n            splitTextNode(selection.startContainer, selection.startOffset, DIRECTIONS.LEFT);\n            startNode = selection.startContainer;\n        }\n\n        const container = this.document.createElement(\"fake-element\");\n        const containerFirstChild = this.document.createElement(\"fake-element-fc\");\n        const containerLastChild = this.document.createElement(\"fake-element-lc\");\n\n        if (typeof content === \"string\") {\n            container.textContent = content;\n        } else {\n            if (content.nodeType === Node.ELEMENT_NODE) {\n                this.dispatchTo(\"normalize_handlers\", content);\n            } else {\n                for (const child of children(content)) {\n                    this.dispatchTo(\"normalize_handlers\", child);\n                }\n            }\n            container.replaceChildren(content);\n        }\n        const allInsertedNodes = [];\n\n        // In case the html inserted starts with a list and will be inserted within\n        // a list, unwrap the list elements from the list.\n        const hasSingleChild = nodeSize(container) === 1;\n        if (\n            closestElement(selection.anchorNode, listElementSelector) &&\n            isListElement(container.firstChild)\n        ) {\n            unwrapContents(container.firstChild);\n        }\n        // Similarly if the html inserted ends with a list.\n        if (\n            closestElement(selection.focusNode, listElementSelector) &&\n            isListElement(container.lastChild) &&\n            !hasSingleChild\n        ) {\n            unwrapContents(container.lastChild);\n        }\n\n        startNode = startNode || this.dependencies.selection.getEditableSelection().anchorNode;\n        const block = closestBlock(selection.anchorNode);\n\n        const shouldUnwrap = (node) =>\n            (isParagraphRelatedElement(node) ||\n                isListItemElement(node) ||\n                // TODO remove: PRE should be a paragraphRelatedElement\n                node.nodeName === \"PRE\") &&\n            !isEmptyBlock(block) &&\n            !isEmptyBlock(node) &&\n            (isContentEditable(node) ||\n                (!node.isConnected && !closestElement(node, \"[contenteditable]\"))) &&\n            !this.dependencies.split.isUnsplittable(node) &&\n            (node.nodeName === block.nodeName ||\n                (this.dependencies.baseContainer.isCandidateForBaseContainer(node) &&\n                    this.dependencies.baseContainer.isCandidateForBaseContainer(block)) ||\n                // TODO add: when PRE is considered as a paragraphRelatedElement\n                // again, consider unwrapping in PRE by re-enabling the\n                // following condition:\n                // block.nodeName === \"PRE\" ||\n                (block.nodeName === \"DIV\" && this.dependencies.split.isUnsplittable(block))) &&\n            // If the selection anchorNode is the editable itself, the content\n            // should not be unwrapped.\n            !this.isEditionBoundary(selection.anchorNode);\n\n        // Empty block must contain a br element to allow cursor placement.\n        if (\n            container.lastElementChild &&\n            isBlock(container.lastElementChild) &&\n            !container.lastElementChild.hasChildNodes()\n        ) {\n            fillEmpty(container.lastElementChild);\n        }\n\n        // In case the html inserted is all contained in a single root <p> or <li>\n        // tag, we take the all content of the <p> or <li> and avoid inserting the\n        // <p> or <li>.\n        if (container.childElementCount === 1 && shouldUnwrap(container.firstChild)) {\n            const nodeToUnwrap = container.firstElementChild;\n            container.replaceChildren(...childNodes(nodeToUnwrap));\n        } else if (container.childElementCount > 1) {\n            const isSelectionAtStart =\n                firstLeaf(block) === selection.anchorNode && selection.anchorOffset === 0;\n            const isSelectionAtEnd =\n                lastLeaf(block) === selection.focusNode &&\n                selection.focusOffset === nodeSize(selection.focusNode);\n            // Grab the content of the first child block and isolate it.\n            if (shouldUnwrap(container.firstChild) && !isSelectionAtStart) {\n                // Unwrap the deepest nested first <li> element in the\n                // container to extract and paste the text content of the list.\n                if (isListItemElement(container.firstChild)) {\n                    const deepestBlock = closestBlock(firstLeaf(container.firstChild));\n                    this.dependencies.split.splitAroundUntil(deepestBlock, container.firstChild);\n                    container.firstElementChild.replaceChildren(...childNodes(deepestBlock));\n                }\n                containerFirstChild.replaceChildren(...childNodes(container.firstElementChild));\n                container.firstElementChild.remove();\n            }\n            // Grab the content of the last child block and isolate it.\n            if (shouldUnwrap(container.lastChild) && !isSelectionAtEnd) {\n                // Unwrap the deepest nested last <li> element in the container\n                // to extract and paste the text content of the list.\n                if (isListItemElement(container.lastChild)) {\n                    const deepestBlock = closestBlock(lastLeaf(container.lastChild));\n                    this.dependencies.split.splitAroundUntil(deepestBlock, container.lastChild);\n                    container.lastElementChild.replaceChildren(...childNodes(deepestBlock));\n                }\n                containerLastChild.replaceChildren(...childNodes(container.lastElementChild));\n                container.lastElementChild.remove();\n            }\n        }\n\n        if (startNode.nodeType === Node.ELEMENT_NODE) {\n            if (selection.anchorOffset === 0) {\n                const textNode = this.document.createTextNode(\"\");\n                if (isSelfClosingElement(startNode)) {\n                    startNode.parentNode.insertBefore(textNode, startNode);\n                } else {\n                    startNode.prepend(textNode);\n                }\n                startNode = textNode;\n                allInsertedNodes.push(textNode);\n            } else {\n                startNode = childNodes(startNode).at(selection.anchorOffset - 1);\n            }\n        }\n\n        // If we have isolated block content, first we split the current focus\n        // element if it's a block then we insert the content in the right places.\n        let currentNode = startNode;\n        const _insertAt = (reference, nodes, insertBefore) => {\n            for (const child of insertBefore ? nodes.reverse() : nodes) {\n                reference[insertBefore ? \"before\" : \"after\"](child);\n                reference = child;\n            }\n        };\n        const lastInsertedNodes = childNodes(containerLastChild);\n        if (containerLastChild.hasChildNodes()) {\n            const toInsert = childNodes(containerLastChild); // Prevent mutation\n            _insertAt(currentNode, [...toInsert], insertBefore);\n            currentNode = insertBefore ? toInsert[0] : currentNode;\n            toInsert[toInsert.length - 1];\n        }\n        const firstInsertedNodes = childNodes(containerFirstChild);\n        if (containerFirstChild.hasChildNodes()) {\n            const toInsert = childNodes(containerFirstChild); // Prevent mutation\n            _insertAt(currentNode, [...toInsert], insertBefore);\n            currentNode = toInsert[toInsert.length - 1];\n            insertBefore = false;\n        }\n        allInsertedNodes.push(...firstInsertedNodes);\n\n        // If all the Html have been isolated, We force a split of the parent element\n        // to have the need new line in the final result\n        if (!container.hasChildNodes()) {\n            if (this.dependencies.split.isUnsplittable(closestBlock(currentNode.nextSibling))) {\n                this.dependencies.lineBreak.insertLineBreakNode({\n                    targetNode: currentNode.nextSibling,\n                    targetOffset: 0,\n                });\n            } else {\n                // If we arrive here, the o_enter index should always be 0.\n                const parent = currentNode.nextSibling.parentElement;\n                const index = childNodes(parent).indexOf(currentNode.nextSibling);\n                this.dependencies.split.splitBlockNode({\n                    targetNode: parent,\n                    targetOffset: index,\n                });\n            }\n        }\n\n        let nodeToInsert;\n        let doesCurrentNodeAllowsP = allowsParagraphRelatedElements(currentNode);\n        const candidatesForRemoval = [];\n        const insertedNodes = childNodes(container);\n        while ((nodeToInsert = container.firstChild)) {\n            if (isBlock(nodeToInsert) && !doesCurrentNodeAllowsP) {\n                // Split blocks at the edges if inserting new blocks (preventing\n                // <p><p>text</p></p> or <li><li>text</li></li> scenarios).\n                while (\n                    !this.isEditionBoundary(currentNode.parentElement) &&\n                    (!allowsParagraphRelatedElements(currentNode.parentElement) ||\n                        (isListItemElement(currentNode.parentElement) &&\n                            !this.dependencies.split.isUnsplittable(nodeToInsert)))\n                ) {\n                    if (this.dependencies.split.isUnsplittable(currentNode.parentElement)) {\n                        // If we have to insert an unsplittable element, we cannot afford to\n                        // unwrap it we need to search for a more suitable spot to put it\n                        if (this.dependencies.split.isUnsplittable(nodeToInsert)) {\n                            currentNode = currentNode.parentElement;\n                            doesCurrentNodeAllowsP = allowsParagraphRelatedElements(currentNode);\n                            continue;\n                        } else {\n                            makeContentsInline(container);\n                            nodeToInsert = container.firstChild;\n                            break;\n                        }\n                    }\n                    let offset = childNodeIndex(currentNode);\n                    if (!insertBefore) {\n                        offset += 1;\n                    }\n                    if (offset) {\n                        const [left, right] = this.dependencies.split.splitElement(\n                            currentNode.parentElement,\n                            offset\n                        );\n                        currentNode = insertBefore ? right : left;\n                        const otherNode = insertBefore ? left : right;\n                        if (isBlock(otherNode)) {\n                            fillShrunkPhrasingParent(otherNode);\n                        }\n                        // After the content insertion, the right-part of a\n                        // split is evaluated for removal, if it is unnecessary\n                        // (to guarantee a paragraph-related element\n                        // after the last unsplittable inserted element).\n                        candidatesForRemoval.push(right);\n                    } else {\n                        if (isBlock(currentNode)) {\n                            fillShrunkPhrasingParent(currentNode);\n                        }\n                        currentNode = currentNode.parentElement;\n                    }\n                    doesCurrentNodeAllowsP = allowsParagraphRelatedElements(currentNode);\n                }\n                if (\n                    isListItemElement(currentNode.parentElement) &&\n                    isBlock(nodeToInsert) &&\n                    this.dependencies.split.isUnsplittable(nodeToInsert)\n                ) {\n                    const br = document.createElement(\"br\");\n                    currentNode[\n                        isEmptyBlock(currentNode) || !isTangible(currentNode) ? \"before\" : \"after\"\n                    ](br);\n                }\n            }\n            // Ensure that all adjacent paragraph elements are converted to\n            // <li> when inserting in a list.\n            const container = closestBlock(currentNode);\n            for (const processor of this.getResource(\"node_to_insert_processors\")) {\n                nodeToInsert = processor({ nodeToInsert, container });\n            }\n            if (insertBefore) {\n                currentNode.before(nodeToInsert);\n                insertBefore = false;\n            } else {\n                currentNode.after(nodeToInsert);\n            }\n            allInsertedNodes.push(nodeToInsert);\n            if (currentNode.tagName !== \"BR\" && isShrunkBlock(currentNode)) {\n                currentNode.remove();\n            }\n            currentNode = nodeToInsert;\n        }\n        allInsertedNodes.push(...lastInsertedNodes);\n        let insertedNodesParents = getConnectedParents(allInsertedNodes);\n        for (const parent of insertedNodesParents) {\n            if (\n                !this.config.allowInlineAtRoot &&\n                this.isEditionBoundary(parent) &&\n                allowsParagraphRelatedElements(parent)\n            ) {\n                // Ensure that edition boundaries do not have inline content.\n                wrapInlinesInBlocks(parent, {\n                    baseContainerNodeName: this.dependencies.baseContainer.getDefaultNodeName(),\n                });\n            }\n        }\n        insertedNodesParents = getConnectedParents(allInsertedNodes);\n        for (const parent of insertedNodesParents) {\n            if (\n                !isProtecting(parent) &&\n                !(isProtected(parent) && !isUnprotecting(parent)) &&\n                parent.isContentEditable\n            ) {\n                cleanTrailingBR(parent, [\n                    (node) => {\n                        // Don't remove the last BR in cases where the\n                        // previous sibling is an unsplittable block\n                        // (i.e. a table, a non-editable div, ...)\n                        // to allow placing the cursor after that unsplittable\n                        // element. This can be removed when the cursor\n                        // is properly handled around these elements.\n                        const previousSibling = node.previousSibling;\n                        return (\n                            previousSibling &&\n                            isBlock(previousSibling) &&\n                            this.dependencies.split.isUnsplittable(previousSibling)\n                        );\n                    },\n                ]);\n            }\n        }\n        for (const candidateForRemoval of candidatesForRemoval) {\n            // Ensure that a paragraph related element is present after the last\n            // unsplittable inserted element\n            if (\n                candidateForRemoval.isConnected &&\n                (isParagraphRelatedElement(candidateForRemoval) ||\n                    isListItemElement(candidateForRemoval)) &&\n                candidateForRemoval.parentElement.isContentEditable &&\n                isEmptyBlock(candidateForRemoval) &&\n                ((candidateForRemoval.previousElementSibling &&\n                    !this.dependencies.split.isUnsplittable(\n                        candidateForRemoval.previousElementSibling\n                    )) ||\n                    (candidateForRemoval.nextElementSibling &&\n                        !this.dependencies.split.isUnsplittable(\n                            candidateForRemoval.nextElementSibling\n                        )))\n            ) {\n                candidateForRemoval.remove();\n            }\n        }\n        for (const insertedNode of allInsertedNodes.reverse()) {\n            if (insertedNode.isConnected) {\n                currentNode = insertedNode;\n                break;\n            }\n        }\n        let lastPosition =\n            isParagraphRelatedElement(currentNode) ||\n            isListItemElement(currentNode) ||\n            isListElement(currentNode)\n                ? rightPos(lastLeaf(currentNode))\n                : rightPos(currentNode);\n        lastPosition = normalizeCursorPosition(lastPosition[0], lastPosition[1], \"right\");\n\n        if (!this.config.allowInlineAtRoot && this.isEditionBoundary(lastPosition[0])) {\n            // Correct the position if it happens to be in the editable root.\n            lastPosition = getDeepestPosition(...lastPosition);\n        }\n        this.dependencies.selection.setSelection(\n            { anchorNode: lastPosition[0], anchorOffset: lastPosition[1] },\n            { normalize: false }\n        );\n        return firstInsertedNodes.concat(insertedNodes).concat(lastInsertedNodes);\n    }\n\n    isEditionBoundary(node) {\n        if (!node) {\n            return false;\n        }\n        if (node === this.editable) {\n            return true;\n        }\n        return isContentEditableAncestor(node);\n    }\n\n    /**\n     * @param {HTMLElement} source\n     * @param {HTMLElement} target\n     */\n    copyAttributes(source, target) {\n        this.dispatchTo(\"clean_handlers\", source);\n        if (source?.nodeType !== Node.ELEMENT_NODE || target?.nodeType !== Node.ELEMENT_NODE) {\n            return;\n        }\n        // TODO: provide a resource to ignore some attributes.\n        const ignoredAttrs = new Set();\n        const ignoredClasses = new Set(this.getResource(\"system_classes\"));\n        for (const attr of source.attributes) {\n            if (ignoredAttrs.has(attr)) {\n                continue;\n            }\n            if (attr.name !== \"class\" || ignoredClasses.size === 0) {\n                target.setAttribute(attr.name, attr.value);\n            } else {\n                const classes = [...source.classList];\n                for (const className of classes) {\n                    if (!ignoredClasses.has(className)) {\n                        target.classList.add(className);\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Basic method to change an element tagName.\n     * It is a technical function which only modifies a tag and its attributes.\n     * It does not modify descendants nor handle the cursor.\n     * @see setTag for the more thorough command.\n     *\n     * @param {HTMLElement} el\n     * @param {string} newTagName\n     */\n    setTagName(el, newTagName) {\n        const document = el.ownerDocument;\n        if (el.tagName === newTagName) {\n            return el;\n        }\n        const newEl = document.createElement(newTagName);\n        const content = childNodes(el);\n        if (isListItemElement(el)) {\n            el.append(newEl);\n            newEl.replaceChildren(...content);\n        } else {\n            if (el.parentElement) {\n                el.before(newEl);\n            }\n            this.copyAttributes(el, newEl);\n            newEl.replaceChildren(...content);\n            el.remove();\n        }\n        return newEl;\n    }\n\n    // --------------------------------------------------------------------------\n    // commands\n    // --------------------------------------------------------------------------\n\n    insertFontAwesome({ faClass = \"fa fa-star\" } = {}) {\n        const fontAwesomeNode = document.createElement(\"i\");\n        fontAwesomeNode.className = faClass;\n        this.insert(fontAwesomeNode);\n        this.dependencies.history.addStep();\n        const [anchorNode, anchorOffset] = rightPos(fontAwesomeNode);\n        this.dependencies.selection.setSelection({ anchorNode, anchorOffset });\n    }\n\n    /**\n     * @param {Object} param0\n     * @param {string} param0.tagName\n     * @param {string} [param0.extraClass]\n     */\n    setTag({ tagName, extraClass = \"\" }) {\n        let newCandidate = this.document.createElement(tagName.toUpperCase());\n        if (extraClass) {\n            newCandidate.classList.add(extraClass);\n        }\n        if (this.dependencies.baseContainer.isCandidateForBaseContainer(newCandidate)) {\n            const baseContainer = this.dependencies.baseContainer.createBaseContainer(\n                newCandidate.nodeName\n            );\n            this.copyAttributes(newCandidate, baseContainer);\n            newCandidate = baseContainer;\n        }\n        const cursors = this.dependencies.selection.preserveSelection();\n        const selectedBlocks = [...this.dependencies.selection.getTraversedBlocks()];\n        const deepestSelectedBlocks = selectedBlocks.filter(\n            (block) =>\n                !descendants(block).some((descendant) => selectedBlocks.includes(descendant)) &&\n                block.isContentEditable\n        );\n        for (const block of deepestSelectedBlocks) {\n            if (\n                isParagraphRelatedElement(block) ||\n                block.nodeName === \"PRE\" || // TODO remove: PRE should be a paragraphRelatedElement\n                isListItemElement(block)\n            ) {\n                if (newCandidate.matches(baseContainerGlobalSelector) && isListItemElement(block)) {\n                    continue;\n                }\n                const newEl = this.setTagName(block, tagName);\n                cursors.remapNode(block, newEl);\n                // We want to be able to edit the case `<h2 class=\"h3\">`\n                // but in that case, we want to display \"Header 2\" and\n                // not \"Header 3\" as it is more important to display\n                // the semantic tag being used (especially for h1 ones).\n                // This is why those are not in `TEXT_STYLE_CLASSES`.\n                const headingClasses = [\"h1\", \"h2\", \"h3\", \"h4\", \"h5\", \"h6\"];\n                removeClass(newEl, ...FONT_SIZE_CLASSES, ...TEXT_STYLE_CLASSES, ...headingClasses);\n                delete newEl.style.fontSize;\n                if (extraClass) {\n                    newEl.classList.add(extraClass);\n                }\n            } else {\n                // eg do not change a <div> into a h1: insert the h1\n                // into it instead.\n                newCandidate.append(...childNodes(block));\n                block.append(newCandidate);\n                cursors.remapNode(block, newCandidate);\n            }\n        }\n        cursors.restore();\n        this.dependencies.history.addStep();\n    }\n\n    insertSeparator() {\n        const selection = this.dependencies.selection.getEditableSelection();\n        const sep = this.document.createElement(\"hr\");\n        const block = closestBlock(selection.startContainer);\n        const element =\n            closestElement(selection.startContainer, paragraphRelatedElementsSelector) ||\n            (block && !isListItemElement(block) ? block : null);\n\n        if (element && element !== this.editable) {\n            element.before(sep);\n        }\n        this.dependencies.history.addStep();\n    }\n\n    removeEmptyClassAndStyleAttributes(root) {\n        for (const node of [root, ...descendants(root)]) {\n            if (node.classList && !node.classList.length) {\n                node.removeAttribute(\"class\");\n            }\n            if (node.style && !node.style.length) {\n                node.removeAttribute(\"style\");\n            }\n        }\n    }\n\n    normalize(el) {\n        if (el.tagName === \"HR\") {\n            el.setAttribute(\n                \"contenteditable\",\n                el.hasAttribute(\"contenteditable\") ? el.getAttribute(\"contenteditable\") : \"false\"\n            );\n        } else {\n            for (const separator of el.querySelectorAll(\"hr\")) {\n                separator.setAttribute(\n                    \"contenteditable\",\n                    separator.hasAttribute(\"contenteditable\")\n                        ? separator.getAttribute(\"contenteditable\")\n                        : \"false\"\n                );\n            }\n        }\n    }\n}\n", "import {\n    htmlEditorVersions,\n    stripVersion,\n    VERSION_SELECTOR,\n} from \"@html_editor/html_migrations/html_migrations_utils\";\nimport { Plugin } from \"@html_editor/plugin\";\n\nexport class EditorVersionPlugin extends Plugin {\n    static id = \"editorVersion\";\n    resources = {\n        clean_for_save_handlers: this.cleanForSave.bind(this),\n        normalize_handlers: this.normalize.bind(this),\n    };\n\n    normalize(element) {\n        if (element.matches(VERSION_SELECTOR) && element !== this.editable) {\n            delete element.dataset.oeVersion;\n        }\n        stripVersion(element);\n    }\n\n    cleanForSave({ root }) {\n        const VERSIONS = htmlEditorVersions();\n        const firstChild = root.firstElementChild;\n        const version = VERSIONS.at(-1);\n        if (firstChild && version) {\n            firstChild.dataset.oeVersion = version;\n        }\n    }\n}\n", "import { Plugin } from \"../plugin\";\nimport { closestBlock, isBlock } from \"../utils/blocks\";\nimport { hasAnyNodesColor } from \"@html_editor/utils/color\";\nimport { cleanTextNode, splitTextNode, unwrapContents } from \"../utils/dom\";\nimport {\n    areSimilarElements,\n    isContentEditable,\n    isEmptyTextNode,\n    isSelfClosingElement,\n    isTextNode,\n    isVisibleTextNode,\n    isZwnbsp,\n    isZWS,\n    previousLeaf,\n} from \"../utils/dom_info\";\nimport { childNodes, closestElement, descendants, selectElements } from \"../utils/dom_traversal\";\nimport { FONT_SIZE_CLASSES, formatsSpecs } from \"../utils/formatting\";\nimport { boundariesIn, boundariesOut, DIRECTIONS, leftPos, rightPos } from \"../utils/position\";\nimport { prepareUpdate } from \"@html_editor/utils/dom_state\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { callbacksForCursorUpdate } from \"@html_editor/utils/selection\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { isFakeLineBreak } from \"../utils/dom_state\";\n\nconst allWhitespaceRegex = /^[\\s\\u200b]*$/;\n\nfunction isFormatted(formatPlugin, format) {\n    return (sel, nodes) => formatPlugin.isSelectionFormat(format, nodes);\n}\n\n/**\n * @typedef {Object} FormatShared\n * @property { FormatPlugin['isSelectionFormat'] } isSelectionFormat\n * @property { FormatPlugin['insertAndSelectZws'] } insertAndSelectZws\n * @property { FormatPlugin['mergeAdjacentInlines'] } mergeAdjacentInlines\n * @property { FormatPlugin['formatSelection'] } formatSelection\n */\n\nexport class FormatPlugin extends Plugin {\n    static id = \"format\";\n    static dependencies = [\"selection\", \"history\", \"input\", \"split\"];\n    // TODO ABD: refactor to handle Knowledge comments inside this plugin without sharing mergeAdjacentInlines.\n    static shared = [\n        \"isSelectionFormat\",\n        \"insertAndSelectZws\",\n        \"mergeAdjacentInlines\",\n        \"formatSelection\",\n    ];\n    resources = {\n        user_commands: [\n            {\n                id: \"formatBold\",\n                title: _t(\"Toggle bold\"),\n                icon: \"fa-bold\",\n                run: this.formatSelection.bind(this, \"bold\"),\n            },\n            {\n                id: \"formatItalic\",\n                title: _t(\"Toggle italic\"),\n                icon: \"fa-italic\",\n                run: this.formatSelection.bind(this, \"italic\"),\n            },\n            {\n                id: \"formatUnderline\",\n                title: _t(\"Toggle underline\"),\n                icon: \"fa-underline\",\n                run: this.formatSelection.bind(this, \"underline\"),\n            },\n            {\n                id: \"formatStrikethrough\",\n                title: _t(\"Toggle strikethrough\"),\n                icon: \"fa-strikethrough\",\n                run: this.formatSelection.bind(this, \"strikeThrough\"),\n            },\n            {\n                id: \"formatFontSize\",\n                run: ({ size }) => {\n                    return this.formatSelection(\"fontSize\", {\n                        applyStyle: true,\n                        formatProps: { size },\n                    });\n                },\n            },\n            {\n                id: \"formatFontSizeClassName\",\n                run: ({ className }) => {\n                    return this.formatSelection(\"setFontSizeClassName\", {\n                        applyStyle: true,\n                        formatProps: { className },\n                    });\n                },\n            },\n            {\n                id: \"removeFormat\",\n                title: _t(\"Remove Format\"),\n                icon: \"fa-eraser\",\n                run: this.removeFormat.bind(this),\n            },\n        ],\n        shortcuts: [\n            { hotkey: \"control+b\", commandId: \"formatBold\" },\n            { hotkey: \"control+i\", commandId: \"formatItalic\" },\n            { hotkey: \"control+u\", commandId: \"formatUnderline\" },\n            { hotkey: \"control+5\", commandId: \"formatStrikethrough\" },\n        ],\n        toolbar_groups: withSequence(20, { id: \"decoration\" }),\n        toolbar_items: [\n            {\n                id: \"bold\",\n                groupId: \"decoration\",\n                commandId: \"formatBold\",\n                isActive: isFormatted(this, \"bold\"),\n            },\n            {\n                id: \"italic\",\n                groupId: \"decoration\",\n                commandId: \"formatItalic\",\n                isActive: isFormatted(this, \"italic\"),\n            },\n            {\n                id: \"underline\",\n                groupId: \"decoration\",\n                commandId: \"formatUnderline\",\n                isActive: isFormatted(this, \"underline\"),\n            },\n            {\n                id: \"strikethrough\",\n                groupId: \"decoration\",\n                commandId: \"formatStrikethrough\",\n                isActive: isFormatted(this, \"strikeThrough\"),\n            },\n            {\n                id: \"remove_format\",\n                groupId: \"decoration\",\n                commandId: \"removeFormat\",\n                isDisabled: (sel, nodes) => !this.hasAnyFormat(nodes),\n            },\n        ],\n        /** Handlers */\n        beforeinput_handlers: withSequence(20, this.onBeforeInput.bind(this)),\n        clean_for_save_handlers: this.cleanForSave.bind(this),\n        normalize_handlers: this.normalize.bind(this),\n\n        intangible_char_for_keyboard_navigation_predicates: (_, char) => char === \"\\u200b\",\n    };\n\n    removeFormat() {\n        const traversedNodes = this.dependencies.selection.getTraversedNodes();\n        for (const format of Object.keys(formatsSpecs)) {\n            if (\n                !formatsSpecs[format].removeStyle ||\n                !this.hasSelectionFormat(format, traversedNodes)\n            ) {\n                continue;\n            }\n            this._formatSelection(format, { applyStyle: false });\n        }\n        this.dispatchTo(\"remove_format_handlers\");\n        this.dependencies.history.addStep();\n    }\n\n    /**\n     * Return true if the current selection on the editable contains a formated\n     * node\n     *\n     * @param {String} format 'bold'|'italic'|'underline'|'strikeThrough'|'switchDirection'\n     * @param {Node[]} [traversedNodes]\n     * @returns {boolean}\n     */\n    hasSelectionFormat(format, traversedNodes = this.dependencies.selection.getTraversedNodes()) {\n        const selectedNodes = traversedNodes.filter(isTextNode);\n        const isFormatted = formatsSpecs[format].isFormatted;\n        return selectedNodes.some((n) => isFormatted(n, this.editable));\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\n     * text node in it appears as that format.\n     *\n     * @param {String} format 'bold'|'italic'|'underline'|'strikeThrough'|'switchDirection'\n     * @param {Node[]} [traversedNodes]\n     * @returns {boolean}\n     */\n    isSelectionFormat(format, traversedNodes = this.dependencies.selection.getTraversedNodes()) {\n        const selectedNodes = traversedNodes.filter(isTextNode);\n        const isFormatted = formatsSpecs[format].isFormatted;\n        return (\n            selectedNodes.length &&\n            selectedNodes.every(\n                (node) =>\n                    isZwnbsp(node) || isEmptyTextNode(node) || isFormatted(node, this.editable)\n            )\n        );\n    }\n\n    // @todo: issues:\n    // - the calls to hasAnyColor should probably be replaced by calls to predicates\n    //   registered as resources (e.g. by the ColorPlugin).\n    hasAnyFormat(traversedNodes) {\n        for (const format of Object.keys(formatsSpecs)) {\n            if (\n                formatsSpecs[format].removeStyle &&\n                this.hasSelectionFormat(format, traversedNodes)\n            ) {\n                return true;\n            }\n        }\n        return (\n            hasAnyNodesColor(traversedNodes, \"color\") ||\n            hasAnyNodesColor(traversedNodes, \"backgroundColor\")\n        );\n    }\n\n    formatSelection(...args) {\n        if (this._formatSelection(...args)) {\n            this.dependencies.history.addStep();\n        }\n    }\n\n    // @todo phoenix: refactor this method.\n    _formatSelection(formatName, { applyStyle, formatProps } = {}) {\n        // note: does it work if selection is in opposite direction?\n        const selection = this.dependencies.split.splitSelection();\n        if (typeof applyStyle === \"undefined\") {\n            applyStyle = !this.isSelectionFormat(formatName);\n        }\n\n        let zws;\n        if (selection.isCollapsed) {\n            if (isTextNode(selection.anchorNode) && selection.anchorNode.textContent === \"\\u200b\") {\n                zws = selection.anchorNode;\n                this.dependencies.selection.setSelection({\n                    anchorNode: zws,\n                    anchorOffset: 0,\n                    focusNode: zws,\n                    focusOffset: 1,\n                });\n            } else {\n                zws = this.insertAndSelectZws();\n            }\n        }\n\n        const selectedNodes = /** @type { Text[] } **/ (\n            this.dependencies.selection\n                .getSelectedNodes()\n                .filter(\n                    (n) =>\n                        ((isTextNode(n) && (isVisibleTextNode(n) || isZWS(n))) ||\n                            (n.nodeName === \"BR\" &&\n                                (isFakeLineBreak(n) ||\n                                    previousLeaf(n, closestBlock(n))?.nodeName === \"BR\"))) &&\n                        isContentEditable(n)\n                )\n        );\n\n        const selectedFieldNodes = new Set(\n            this.dependencies.selection\n                .getSelectedNodes()\n                .map((n) => closestElement(n, \"*[t-field],*[t-out],*[t-esc]\"))\n                .filter(Boolean)\n        );\n        const formatSpec = formatsSpecs[formatName];\n        for (const node of selectedNodes) {\n            const inlineAncestors = [];\n            /** @type { Node } */\n            let currentNode = node;\n            let parentNode = node.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\n            while (\n                parentNode &&\n                !isBlock(parentNode) &&\n                !this.dependencies.split.isUnsplittable(parentNode) &&\n                (parentNode.classList.length === 0 ||\n                    [...parentNode.classList].every((cls) => FONT_SIZE_CLASSES.includes(cls)))\n            ) {\n                const isUselessZws =\n                    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 = this.dependencies.split.splitAroundUntil(\n                        currentNode,\n                        parentNode\n                    );\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 &&\n                    formatSpec.addNeutralStyle(getOrCreateSpan(node, inlineAncestors));\n            } else if (!firstBlockOrClassHasFormat && applyStyle) {\n                const tag = formatSpec.tagName && this.document.createElement(formatSpec.tagName);\n                if (tag) {\n                    node.after(tag);\n                    tag.append(node);\n\n                    if (!formatSpec.isFormatted(tag, formatProps)) {\n                        tag.after(node);\n                        tag.remove();\n                        formatSpec.addStyle(getOrCreateSpan(node, inlineAncestors), formatProps);\n                    }\n                } else if (formatName !== \"fontSize\" || formatProps.size !== undefined) {\n                    formatSpec.addStyle(getOrCreateSpan(node, 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                selectedNodes.includes(siblings[0]) &&\n                selectedNodes.includes(siblings[siblings.length - 1])\n            ) {\n                zws.parentElement.setAttribute(\"data-oe-zws-empty-inline\", \"\");\n            } else {\n                const span = this.document.createElement(\"span\");\n                span.setAttribute(\"data-oe-zws-empty-inline\", \"\");\n                zws.before(span);\n                span.append(zws);\n            }\n        }\n\n        if (\n            selectedNodes.length === 1 &&\n            selectedNodes[0] &&\n            selectedNodes[0].textContent === \"\\u200B\"\n        ) {\n            this.dependencies.selection.setCursorStart(selectedNodes[0]);\n        } else if (selectedNodes.length) {\n            const firstNode = selectedNodes[0];\n            const lastNode = selectedNodes[selectedNodes.length - 1];\n            let newSelection;\n            if (selection.direction === DIRECTIONS.RIGHT) {\n                newSelection = {\n                    anchorNode: firstNode,\n                    anchorOffset: 0,\n                    focusNode: lastNode,\n                    focusOffset: lastNode.length,\n                };\n            } else {\n                newSelection = {\n                    anchorNode: lastNode,\n                    anchorOffset: lastNode.length,\n                    focusNode: firstNode,\n                    focusOffset: 0,\n                };\n            }\n            this.dependencies.selection.setSelection(newSelection, { normalize: false });\n            return true;\n        }\n        if (selectedFieldNodes.size > 0) {\n            return true;\n        }\n    }\n\n    normalize(root) {\n        for (const el of selectElements(root, \"[data-oe-zws-empty-inline]\")) {\n            if (!allWhitespaceRegex.test(el.textContent)) {\n                // The element has some meaningful text. Remove the ZWS in it.\n                delete el.dataset.oeZwsEmptyInline;\n                this.cleanZWS(el);\n                if (\n                    el.tagName === \"SPAN\" &&\n                    el.getAttributeNames().length === 0 &&\n                    el.classList.length === 0\n                ) {\n                    // Useless span, unwrap it.\n                    unwrapContents(el);\n                }\n            }\n        }\n        this.mergeAdjacentInlines(root);\n    }\n\n    cleanForSave({ root, preserveSelection = false } = {}) {\n        for (const element of root.querySelectorAll(\"[data-oe-zws-empty-inline]\")) {\n            this.cleanElement(element, { preserveSelection });\n        }\n        this.mergeAdjacentInlines(root, { preserveSelection });\n    }\n\n    cleanElement(element, { preserveSelection }) {\n        delete element.dataset.oeZwsEmptyInline;\n        if (!allWhitespaceRegex.test(element.textContent)) {\n            // The element has some meaningful text. Remove the ZWS in it.\n            this.cleanZWS(element, { preserveSelection });\n            return;\n        }\n        if (this.getResource(\"unremovable_node_predicates\").some((p) => p(element))) {\n            return;\n        }\n        if (element.classList.length) {\n            // Original comment from web_editor:\n            // We only remove the empty element if it has no class, to ensure we\n            // don't break visual styles (in that case, its ZWS was kept to\n            // ensure the cursor can be placed in it).\n            return;\n        }\n        const restore = prepareUpdate(...leftPos(element), ...rightPos(element));\n        element.remove();\n        restore();\n    }\n\n    cleanZWS(element, { preserveSelection = true } = {}) {\n        const textNodes = descendants(element).filter(isTextNode);\n        const cursors = preserveSelection ? this.dependencies.selection.preserveSelection() : null;\n        for (const node of textNodes) {\n            cleanTextNode(node, \"\\u200B\", cursors);\n        }\n        cursors?.restore();\n    }\n\n    insertText(selection, content) {\n        if (selection.anchorNode.nodeType === Node.TEXT_NODE) {\n            selection = this.dependencies.selection.setSelection(\n                {\n                    anchorNode: selection.anchorNode.parentElement,\n                    anchorOffset: splitTextNode(selection.anchorNode, selection.anchorOffset),\n                },\n                { normalize: false }\n            );\n        }\n\n        const txt = this.document.createTextNode(content || \"#\");\n        const restore = prepareUpdate(selection.anchorNode, selection.anchorOffset);\n        selection.anchorNode.insertBefore(\n            txt,\n            selection.anchorNode.childNodes[selection.anchorOffset]\n        );\n        restore();\n        const [anchorNode, anchorOffset, focusNode, focusOffset] = boundariesOut(txt);\n        this.dependencies.selection.setSelection(\n            { anchorNode, anchorOffset, focusNode, focusOffset },\n            { normalize: false }\n        );\n        return txt;\n    }\n\n    /**\n     * Use the actual selection (assumed to be collapsed) and insert a\n     * zero-width space at its anchor point. Then, select that zero-width\n     * space.\n     *\n     * @returns {Node} the inserted zero-width space\n     */\n    insertAndSelectZws() {\n        const selection = this.dependencies.selection.getEditableSelection();\n        const zws = this.insertText(selection, \"\\u200B\");\n        splitTextNode(zws, selection.anchorOffset);\n        return zws;\n    }\n\n    onBeforeInput(ev) {\n        if (ev.inputType === \"insertText\") {\n            const selection = this.dependencies.selection.getEditableSelection();\n            if (!selection.isCollapsed) {\n                return;\n            }\n            const element = closestElement(selection.anchorNode);\n            if (element.hasAttribute(\"data-oe-zws-empty-inline\")) {\n                // Select its ZWS content to make sure the text will be\n                // inserted inside the element, and not before (outside) it.\n                // This addresses an undesired behavior of the\n                // contenteditable.\n                const [anchorNode, anchorOffset, focusNode, focusOffset] = boundariesIn(element);\n                this.dependencies.selection.setSelection({\n                    anchorNode,\n                    anchorOffset,\n                    focusNode,\n                    focusOffset,\n                });\n            }\n        }\n    }\n\n    /**\n     * @param {Node} root\n     * @param {Object} [options]\n     * @param {boolean} [options.preserveSelection=true]\n     */\n    mergeAdjacentInlines(root, { preserveSelection = true } = {}) {\n        let selectionToRestore = null;\n        for (const node of descendants(root)) {\n            if (this.shouldBeMergedWithPreviousSibling(node)) {\n                if (preserveSelection) {\n                    selectionToRestore ??= this.dependencies.selection.preserveSelection();\n                    selectionToRestore.update(callbacksForCursorUpdate.merge(node));\n                }\n                node.previousSibling.append(...childNodes(node));\n                node.remove();\n            }\n        }\n        selectionToRestore?.restore();\n    }\n\n    shouldBeMergedWithPreviousSibling(node) {\n        const isMergeable = (node) =>\n            !this.getResource(\"unsplittable_node_predicates\").some((predicate) => predicate(node));\n        return (\n            !isSelfClosingElement(node) &&\n            areSimilarElements(node, node.previousSibling) &&\n            isMergeable(node)\n        );\n    }\n}\n\nfunction getOrCreateSpan(node, ancestors) {\n    const document = node.ownerDocument;\n    const span = ancestors.find((element) => element.tagName === \"SPAN\" && element.isConnected);\n    const lastInlineAncestor = ancestors.findLast(\n        (element) => !isBlock(element) && element.isConnected\n    );\n    if (span) {\n        return span;\n    } else {\n        const span = document.createElement(\"span\");\n        // Apply font span above current inline top ancestor so that\n        // the font style applies to the other style tags as well.\n        if (lastInlineAncestor) {\n            lastInlineAncestor.after(span);\n            span.append(lastInlineAncestor);\n        } else {\n            node.after(span);\n            span.append(node);\n        }\n        return span;\n    }\n}\nfunction removeFormat(node, formatSpec) {\n    const document = node.ownerDocument;\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", "import { Plugin } from \"../plugin\";\nimport { childNodes, descendants, getCommonAncestor } from \"../utils/dom_traversal\";\n\n/**\n * @typedef { import(\"./selection_plugin\").EditorSelection } EditorSelection\n *\n * @typedef { Object } SerializedSelection\n * @property { string } anchorNodeId\n * @property { number } anchorOffset\n * @property { string } focusNodeId\n * @property { number } focusOffset\n *\n * @typedef { Object } SerializedNode\n * @property { number } nodeType\n * @property { string } nodeId\n * @property { string } textValue\n * @property { string } tagName\n * @property { SerializedNode[] } children\n * @property { Record<string, string> } attributes\n *\n * @typedef { Object } HistoryStep\n * @property { string } id\n * @property { SerializedSelection } selection\n * @property { HistoryMutation[] } mutations\n * @property { string } previousStepId\n *\n * @typedef { Object } HistoryMutationCharacterData\n * @property { \"characterData\" } type\n * // todo change id to nodeId\n * @property { string } id\n * // todo change text to textValue\n * @property { string } text\n * // todo change text to textOldValue\n * @property { string } oldValue\n *\n * @typedef { Object } HistoryMutationAttributes\n * @property { \"attributes\" } type\n * // todo change id to nodeId\n * @property { string } id\n * @property { string } attributeName\n * // todo change value to attributeValue\n * @property { string } value\n * // todo change oldValue to attributeOldValue\n * @property { string } oldValue\n *\n * @typedef { Object } HistoryMutationAdd\n * @property { \"add\" } type\n * // todo change id to nodeId\n * @property { string } id\n * @property { string } node\n * // todo change prepend to prependNodeId\n * @property { string } prepend\n * // todo change append to appendNodeId\n * @property { string } append\n * // todo change before to beforeNodeId\n * @property { string } before\n * // todo change after to afterNodeId\n * @property { string } after\n *\n * @typedef { Object } HistoryMutationRemove\n * @property { \"remove\" } type\n * // todo change id to nodeId\n * @property { string } id\n * // todo change parentId to parentNodeId\n * @property { string } parentId\n * @property { Node } node\n * // todo change nextId to nextNodeId\n * @property { string } nextId\n * // todo change previousId to previousNodeId\n * @property { string } previousId\n *\n * @typedef { HistoryMutationCharacterData | HistoryMutationAttributes | HistoryMutationAdd | HistoryMutationRemove } HistoryMutation\n *\n * @typedef { Object } PreviewableOperation\n * @property { Function } apply\n * @property { Function } preview\n * @property { Function } revert\n */\n\n/**\n * @typedef { Object } HistoryShared\n * @property { HistoryPlugin['addExternalStep'] } addExternalStep\n * @property { HistoryPlugin['addStep'] } addStep\n * @property { HistoryPlugin['canRedo'] } canRedo\n * @property { HistoryPlugin['canUndo'] } canUndo\n * @property { HistoryPlugin['disableObserver'] } disableObserver\n * @property { HistoryPlugin['enableObserver'] } enableObserver\n * @property { HistoryPlugin['getHistorySteps'] } getHistorySteps\n * @property { HistoryPlugin['getNodeById'] } getNodeById\n * @property { HistoryPlugin['makePreviewableOperation'] } makePreviewableOperation\n * @property { HistoryPlugin['makeSavePoint'] } makeSavePoint\n * @property { HistoryPlugin['makeSnapshotStep'] } makeSnapshotStep\n * @property { HistoryPlugin['redo'] } redo\n * @property { HistoryPlugin['reset'] } reset\n * @property { HistoryPlugin['resetFromSteps'] } resetFromSteps\n * @property { HistoryPlugin['serializeSelection'] } serializeSelection\n * @property { HistoryPlugin['stageSelection'] } stageSelection\n * @property { HistoryPlugin['undo'] } undo\n */\n\nexport class HistoryPlugin extends Plugin {\n    static id = \"history\";\n    static dependencies = [\"selection\", \"sanitize\"];\n    static shared = [\n        \"addExternalStep\",\n        \"addStep\",\n        \"canRedo\",\n        \"canUndo\",\n        \"disableObserver\",\n        \"enableObserver\",\n        \"getHistorySteps\",\n        \"getNodeById\",\n        \"makePreviewableOperation\",\n        \"makeSavePoint\",\n        \"makeSnapshotStep\",\n        \"redo\",\n        \"reset\",\n        \"resetFromSteps\",\n        \"serializeSelection\",\n        \"stageSelection\",\n        \"undo\",\n    ];\n    resources = {\n        user_commands: [\n            { id: \"historyUndo\", run: this.undo.bind(this) },\n            { id: \"historyRedo\", run: this.redo.bind(this) },\n        ],\n        shortcuts: [\n            { hotkey: \"control+z\", commandId: \"historyUndo\" },\n            { hotkey: \"control+y\", commandId: \"historyRedo\" },\n            { hotkey: \"control+shift+z\", commandId: \"historyRedo\" },\n        ],\n        start_edition_handlers: () => {\n            this.enableObserver();\n            this.reset(this.config.content);\n        },\n    };\n\n    setup() {\n        this.mutationFilteredClasses = new Set(this.getResource(\"system_classes\"));\n        this._onKeyupResetContenteditableNodes = [];\n        this.addDomListener(this.document, \"beforeinput\", this._onDocumentBeforeInput.bind(this));\n        this.addDomListener(this.document, \"input\", this._onDocumentInput.bind(this));\n        this.addDomListener(this.editable, \"pointerup\", () => {\n            this.stageSelection();\n        });\n        this.observer = new MutationObserver(this.handleNewRecords.bind(this));\n        this._cleanups.push(() => this.observer.disconnect());\n        this.clean();\n    }\n\n    clean() {\n        this.handleObserverRecords();\n        /** @type { HistoryStep[] } */\n        this.steps = [];\n        /** @type { HistoryStep } */\n        this.currentStep = this.processHistoryStep({\n            selection: {},\n            mutations: [],\n            id: this.generateId(),\n            previousStepId: undefined,\n        });\n        /** @type { Map<string, \"consumed\"|\"undo\"|\"redo\"> } */\n        this.stepsStates = new Map();\n        this.nodeToIdMap = new WeakMap();\n        this.idToNodeMap = new Map();\n        this.setNodeId(this.editable);\n        this.dispatchTo(\"history_cleaned_handlers\");\n    }\n    /**\n     * @param {string} id\n     * @returns {Node}\n     */\n    getNodeById(id) {\n        return this.idToNodeMap.get(id);\n    }\n    /**\n     * Reset the history.\n     *\n     * @param { string } content\n     */\n    reset(content) {\n        this.clean();\n        this.stageSelection();\n        this.steps.push(this.makeSnapshotStep());\n        this.dispatchTo(\"history_reset_handlers\", content);\n    }\n    /**\n     * @param { HistoryStep[] } steps\n     */\n    resetFromSteps(steps) {\n        this.disableObserver();\n        this.editable.replaceChildren();\n        this.clean();\n        this.stageSelection();\n        for (const step of steps) {\n            this.applyMutations(step.mutations);\n        }\n        this.steps = steps;\n        // todo: to test\n        this.dispatchTo(\"history_reset_from_steps_handlers\");\n\n        this.enableObserver();\n        this.dispatchTo(\"history_reset_from_steps_handlers\");\n    }\n    makeSnapshotStep() {\n        return {\n            selection: {\n                anchorNode: undefined,\n                anchorOffset: undefined,\n                focusNode: undefined,\n                focusOffset: undefined,\n            },\n            mutations: childNodes(this.editable).map((node) => ({\n                type: \"add\",\n                append: \"root\",\n                id: this.nodeToIdMap.get(node),\n                node: this.serializeNode(node),\n            })),\n            id: this.steps[this.steps.length - 1]?.id || this.generateId(),\n            previousStepId: undefined,\n        };\n    }\n\n    getHistorySteps() {\n        return this.steps;\n    }\n    /**\n     * @param { HistoryStep } step\n     */\n    processHistoryStep(step) {\n        for (const fn of this.getResource(\"history_step_processors\")) {\n            step = fn(step);\n        }\n        return step;\n    }\n\n    enableObserver() {\n        this.observer.observe(this.editable, {\n            childList: true,\n            subtree: true,\n            attributes: true,\n            attributeOldValue: true,\n            characterData: true,\n            characterDataOldValue: true,\n        });\n    }\n    disableObserver() {\n        // @todo @phoenix do we still want to unobserve sometimes?\n        this.handleObserverRecords();\n        this.observer.disconnect();\n    }\n\n    handleObserverRecords() {\n        this.handleNewRecords(this.observer.takeRecords());\n    }\n\n    /**\n     * @param { MutationRecord[] } records\n     * @returns { MutationRecord[] } processed records\n     */\n    processNewRecords(records) {\n        this.setIdOnRecords(records);\n        records = this.filterMutationRecords(records);\n        if (!records.length) {\n            return [];\n        }\n        this.getResource(\"handleNewRecords\").forEach((cb) => cb(records));\n        this.stageRecords(records);\n        return records;\n    }\n\n    dispatchContentUpdated() {\n        if (!this.currentStep?.mutations?.length) {\n            return;\n        }\n        // @todo @phoenix remove this?\n        // @todo @phoenix this includes previous mutations that were already\n        // stored in the current step. Ideally, it should only include the new ones.\n        const root = this.getMutationsRoot(this.currentStep.mutations);\n        if (!root) {\n            return;\n        }\n        this.dispatchTo(\"content_updated_handlers\", root);\n    }\n\n    /**\n     * @param { MutationRecord[] } records\n     */\n    handleNewRecords(records) {\n        if (this.processNewRecords(records).length) {\n            this.dispatchContentUpdated();\n        }\n    }\n\n    /**\n     * @param { MutationRecord[] } records\n     */\n    setIdOnRecords(records) {\n        for (const record of records) {\n            if (record.type === \"childList\" && record.addedNodes.length) {\n                for (const node of record.addedNodes) {\n                    this.setNodeId(node);\n                }\n            }\n        }\n    }\n    /**\n     * @param { MutationRecord[] } records\n     */\n    filterMutationRecords(records) {\n        this.dispatchTo(\"before_filter_mutation_record_handlers\", records);\n        for (const callback of this.getResource(\"savable_mutation_record_predicates\")) {\n            records = records.filter(callback);\n        }\n\n        // Save the first attribute in a cache to compare only the first\n        // attribute record of node to its latest state.\n        const attributeCache = new Map();\n        const filteredRecords = [];\n\n        for (const record of records) {\n            if (record.type === \"attributes\") {\n                // Skip the attributes change on the dom.\n                if (record.target === this.editable) {\n                    continue;\n                }\n                if (record.attributeName === \"contenteditable\") {\n                    continue;\n                }\n                // @todo @phoenix test attributeCache\n                attributeCache.set(record.target, attributeCache.get(record.target) || {});\n                // @todo @phoenix add test for mutationFilteredClasses.\n                if (record.attributeName === \"class\") {\n                    const classBefore = (record.oldValue && record.oldValue.split(\" \")) || [];\n                    const classAfter =\n                        (record.target.className &&\n                            record.target.className.split &&\n                            record.target.className.split(\" \")) ||\n                        [];\n                    const excludedClasses = [];\n                    for (const klass of classBefore) {\n                        if (!classAfter.includes(klass)) {\n                            excludedClasses.push(klass);\n                        }\n                    }\n                    for (const klass of classAfter) {\n                        if (!classBefore.includes(klass)) {\n                            excludedClasses.push(klass);\n                        }\n                    }\n                    if (\n                        excludedClasses.length &&\n                        excludedClasses.every((c) => this.mutationFilteredClasses.has(c))\n                    ) {\n                        continue;\n                    }\n                }\n                if (\n                    typeof attributeCache.get(record.target)[record.attributeName] === \"undefined\"\n                ) {\n                    const oldValue = record.oldValue === undefined ? null : record.oldValue;\n                    attributeCache.get(record.target)[record.attributeName] =\n                        oldValue !== record.target.getAttribute(record.attributeName);\n                }\n                if (!attributeCache.get(record.target)[record.attributeName]) {\n                    continue;\n                }\n            }\n            filteredRecords.push(record);\n        }\n        // @todo @phoenix allow an option to filter mutation records.\n        return filteredRecords;\n    }\n\n    /**\n     * Set the serialized selection of the currentStep.\n     *\n     * This method is used to save a serialized selection in the currentStep.\n     * It will be necessary if the step is reverted at some point because we need\n     * to set the selection to where it was before any mutation was made.\n     *\n     * It means that we should not call this method in the middle of mutations\n     * because if a selection is set onto a node that is edited/added/removed\n     * within the same step, it might become impossible to set the selection\n     * when reverting the step.\n     */\n    stageSelection() {\n        const selection = this.dependencies.selection.getEditableSelection();\n        if (\n            this.currentStep.mutations.find((m) =>\n                [\"characterData\", \"remove\", \"add\"].includes(m.type)\n            )\n        ) {\n            console.warn(\n                `should not have any \"characterData\", \"remove\" or \"add\" mutations in current step when you update the selection`\n            );\n            return;\n        }\n        this.currentStep.selection = this.serializeSelection(selection);\n    }\n    /**\n     * @param { MutationRecord[] } records\n     */\n    stageRecords(records) {\n        // @todo @phoenix test this feature.\n        // There is a case where node A is added and node B is a descendant of\n        // node A where node B was not in the observed tree) then node B is\n        // added into another node. In that case, we need to keep track of node\n        // B so when serializing node A, we strip node B from the node A tree to\n        // avoid the duplication of node A.\n        const mutatedNodes = new Set();\n        for (const record of records) {\n            if (record.type === \"childList\") {\n                for (const node of record.addedNodes) {\n                    const id = this.setNodeId(node);\n                    mutatedNodes.add(id);\n                }\n                for (const node of record.removedNodes) {\n                    const id = this.setNodeId(node);\n                    mutatedNodes.delete(id);\n                }\n            }\n        }\n        for (const record of records) {\n            switch (record.type) {\n                case \"characterData\": {\n                    this.currentStep.mutations.push({\n                        type: \"characterData\",\n                        id: this.nodeToIdMap.get(record.target),\n                        text: record.target.textContent,\n                        oldValue: record.oldValue,\n                    });\n                    break;\n                }\n                case \"attributes\": {\n                    this.currentStep.mutations.push({\n                        type: \"attributes\",\n                        id: this.nodeToIdMap.get(record.target),\n                        attributeName: record.attributeName,\n                        value: record.target.getAttribute(record.attributeName),\n                        oldValue: record.oldValue,\n                    });\n                    this.dispatchTo(\"attribute_change_handlers\", {\n                        target: record.target,\n                        attributeName: record.attributeName,\n                        oldValue: record.oldValue,\n                        value: record.target.getAttribute(record.attributeName),\n                    });\n                    break;\n                }\n                case \"childList\": {\n                    record.addedNodes.forEach((added) => {\n                        const mutation = {\n                            type: \"add\",\n                        };\n                        if (!record.nextSibling && this.nodeToIdMap.get(record.target)) {\n                            mutation.append = this.nodeToIdMap.get(record.target);\n                        } else if (record.nextSibling && this.nodeToIdMap.get(record.nextSibling)) {\n                            mutation.before = this.nodeToIdMap.get(record.nextSibling);\n                        } else if (!record.previousSibling && this.nodeToIdMap.get(record.target)) {\n                            mutation.prepend = this.nodeToIdMap.get(record.target);\n                        } else if (\n                            record.previousSibling &&\n                            this.nodeToIdMap.get(record.previousSibling)\n                        ) {\n                            mutation.after = this.nodeToIdMap.get(record.previousSibling);\n                        } else {\n                            return false;\n                        }\n                        mutation.id = this.nodeToIdMap.get(added);\n                        mutation.node = this.serializeNode(added, mutatedNodes);\n                        this.currentStep.mutations.push(mutation);\n                    });\n                    record.removedNodes.forEach((removed) => {\n                        this.currentStep.mutations.push({\n                            type: \"remove\",\n                            id: this.nodeToIdMap.get(removed),\n                            parentId: this.nodeToIdMap.get(record.target),\n                            node: this.serializeNode(removed),\n                            nextId: record.nextSibling\n                                ? this.nodeToIdMap.get(record.nextSibling)\n                                : undefined,\n                            previousId: record.previousSibling\n                                ? this.nodeToIdMap.get(record.previousSibling)\n                                : undefined,\n                        });\n                    });\n                    break;\n                }\n            }\n        }\n    }\n\n    /**\n     * @param { Node } node\n     */\n    setNodeId(node) {\n        let id = this.nodeToIdMap.get(node);\n        if (!id) {\n            id = node === this.editable ? \"root\" : this.generateId();\n            this.nodeToIdMap.set(node, id);\n            this.idToNodeMap.set(id, node);\n            node = node.firstChild;\n            while (node) {\n                this.setNodeId(node);\n                node = node.nextSibling;\n            }\n        }\n        return id;\n    }\n    generateId() {\n        // No need for secure random number.\n        return Math.floor(Math.random() * Math.pow(2, 52)).toString();\n    }\n\n    /**\n     * @param { Object } [params]\n     * @param { \"consumed\"|\"undo\"|\"redo\" } [params.stepState]\n     */\n    addStep({ stepState } = {}) {\n        // @todo @phoenix should we allow to pause the making of a step?\n        // if (!this.stepsActive) {\n        //     return;\n        // }\n        // @todo @phoenix link zws plugin\n        // this._resetLinkZws();\n        // @todo @phoenix sanitize plugin\n        // this.sanitize();\n\n        this.handleObserverRecords();\n        const currentStep = this.currentStep;\n        const currentMutationsCount = currentStep.mutations.length;\n        if (currentMutationsCount === 0) {\n            return false;\n        }\n        const stepCommonAncestor = this.getMutationsRoot(currentStep.mutations) || this.editable;\n        this.dispatchTo(\"normalize_handlers\", stepCommonAncestor);\n        this.handleObserverRecords();\n        if (currentMutationsCount === currentStep.mutations.length) {\n            // If there was no registered mutation during the normalization step,\n            // force the dispatch of a content_updated to allow i.e. the hint\n            // plugin to react to non-observed changes (i.e. a div becoming\n            // a baseContainer).\n            this.dispatchContentUpdated();\n        }\n\n        currentStep.previousStepId = this.steps.at(-1)?.id;\n\n        this.steps.push(currentStep);\n        // @todo @phoenix add this in the linkzws plugin.\n        // this._setLinkZws();\n        this.currentStep = this.processHistoryStep({\n            id: this.generateId(),\n            selection: {},\n            mutations: [],\n            previousStepId: undefined,\n        });\n        // Set the state of the step here.\n        // That way, the state of undo and redo is truly accessible\n        // when executing the onChange callback.\n        // It is useful for external components if they execute shared.can[Undo|Redo]\n        if (stepState) {\n            this.stepsStates.set(currentStep.id, stepState);\n        }\n        this.stageSelection();\n        this.dispatchTo(\"step_added_handlers\", { step: currentStep, stepCommonAncestor });\n        this.config.onChange?.();\n        return currentStep;\n    }\n    canUndo() {\n        return this.getNextUndoIndex() > 0;\n    }\n    canRedo() {\n        return this.getNextRedoIndex() > 0;\n    }\n    undo() {\n        if (this.steps.length === 1) {\n            return;\n        }\n        this.handleObserverRecords();\n        // The last step is considered an uncommited draft so always revert it.\n        const lastStep = this.currentStep;\n        this.revertMutations(lastStep.mutations);\n        // Discard mutations generated by the revert.\n        this.observer.takeRecords();\n        // Clean the last step otherwise if no other step is created after, the\n        // mutations of the revert itself will be added to the same step and\n        // grow exponentially at each undo.\n        lastStep.mutations = [];\n\n        const pos = this.getNextUndoIndex();\n        if (pos > 0) {\n            // Consider the position consumed.\n            this.stepsStates.set(this.steps[pos].id, \"consumed\");\n            this.revertMutations(this.steps[pos].mutations, { forNewStep: true });\n            this.setSerializedSelection(this.steps[pos].selection);\n            this.addStep({ stepState: \"undo\" });\n            // Consider the last position of the history as an undo.\n        }\n        this.dispatchTo(\"post_undo_handlers\");\n    }\n    redo() {\n        this.handleObserverRecords();\n        // Current step is considered an uncommitted draft, so revert it,\n        // otherwise a redo would not be possible.\n        this.revertMutations(this.currentStep.mutations);\n        // Discard mutations generated by the revert.\n        this.observer.takeRecords();\n        // At this point, _currentStep.mutations contains the current step's\n        // mutations plus the ones that revert it, with net effect zero.\n        this.currentStep.mutations = [];\n\n        const pos = this.getNextRedoIndex();\n        if (pos > 0) {\n            this.stepsStates.set(this.steps[pos].id, \"consumed\");\n            this.revertMutations(this.steps[pos].mutations, { forNewStep: true });\n            this.setSerializedSelection(this.steps[pos].selection);\n            this.addStep({ stepState: \"redo\" });\n        }\n        this.dispatchTo(\"post_redo_handlers\");\n    }\n    /**\n     * @param { SerializedSelection } selection\n     */\n    setSerializedSelection(selection) {\n        if (!selection.anchorNodeId) {\n            return;\n        }\n        const anchorNode = this.idToNodeMap.get(selection.anchorNodeId);\n        if (!anchorNode) {\n            return;\n        }\n        const newSelection = {\n            anchorNode,\n            anchorOffset: selection.anchorOffset,\n        };\n        const focusNode = this.idToNodeMap.get(selection.focusNodeId);\n        if (focusNode) {\n            newSelection.focusNode = focusNode;\n            newSelection.focusOffset = selection.focusOffset;\n        }\n        this.dependencies.selection.setSelection(newSelection, { normalize: false });\n        // @todo @phoenix add this in the selection or table plugin.\n        // // If a table must be selected, ensure it's in the same tick.\n        // this._handleSelectionInTable();\n    }\n    /**\n     * Get the step index in the history to undo.\n     * Return -1 if no undo index can be found.\n     */\n    getNextUndoIndex() {\n        // Go back to first step that can be undone (\"redo\" or undefined).\n        for (let index = this.steps.length - 1; index >= 0; index--) {\n            if (this.isReversibleStep(index)) {\n                const state = this.stepsStates.get(this.steps[index].id);\n                if (state === \"redo\" || !state) {\n                    return index;\n                }\n            }\n        }\n        // There is no steps left to be undone, return an index that does not\n        // point to any step\n        return -1;\n    }\n    /**\n     * Meant to be overriden.\n     *\n     * @param { number } index\n     */\n    isReversibleStep(index) {\n        const step = this.steps[index];\n        if (!step) {\n            return false;\n        }\n        return !this.getResource(\"unreversible_step_predicates\").some((predicate) =>\n            predicate(step)\n        );\n    }\n    /**\n     * Get the step index in the history to redo.\n     * Return -1 if no redo index can be found.\n     */\n    getNextRedoIndex() {\n        // We cannot redo more than what is consumed.\n        // Check if we have no more \"consumed\" than \"redo\" until we get to an\n        // \"undo\"\n        let totalConsumed = 0;\n        for (let index = this.steps.length - 1; index >= 0; index--) {\n            if (this.isReversibleStep(index)) {\n                const state = this.stepsStates.get(this.steps[index].id);\n                switch (state) {\n                    case \"undo\":\n                        return totalConsumed <= 0 ? index : -1;\n                    case \"redo\":\n                        totalConsumed -= 1;\n                        break;\n                    case \"consumed\":\n                        totalConsumed += 1;\n                        break;\n                    default:\n                        return -1;\n                }\n            }\n        }\n        return -1;\n    }\n    /**\n     * Insert a step in the history.\n     *\n     * @param { HistoryStep } newStep\n     * @param { number } index\n     */\n    addExternalStep(newStep, index) {\n        // The last step is an uncommited draft, revert it first\n        this.revertMutations(this.currentStep.mutations);\n\n        const stepsAfterNewStep = this.steps.slice(index);\n\n        for (const stepToRevert of stepsAfterNewStep.slice().reverse()) {\n            this.revertMutations(stepToRevert.mutations);\n        }\n        this.applyMutations(newStep.mutations);\n        this.dispatchTo(\n            \"normalize_handlers\",\n            this.getMutationsRoot(newStep.mutations) || this.editable\n        );\n        this.steps.splice(index, 0, newStep);\n        for (const stepToApply of stepsAfterNewStep) {\n            this.applyMutations(stepToApply.mutations);\n        }\n        // Reapply the uncommited draft, since this is not an operation which should cancel it\n        this.applyMutations(this.currentStep.mutations);\n        this.dispatchTo(\"external_step_added_handlers\");\n    }\n    /**\n     * @param { HistoryMutation[] } mutations\n     * @param { Object } options\n     * @param { boolean } options.forNewStep whether the mutations will be used\n     *        to create a new step\n     */\n    applyMutations(mutations, { forNewStep = false } = {}) {\n        for (const mutation of mutations) {\n            switch (mutation.type) {\n                case \"characterData\": {\n                    const node = this.idToNodeMap.get(mutation.id);\n                    if (node) {\n                        node.textContent = mutation.text;\n                    }\n                    break;\n                }\n                case \"attributes\": {\n                    const node = this.idToNodeMap.get(mutation.id);\n                    if (node) {\n                        let value = this.getAttributeValue(mutation.attributeName, mutation.value);\n                        for (const cb of this.getResource(\"attribute_change_processors\")) {\n                            value = cb(\n                                {\n                                    target: node,\n                                    attributeName: mutation.attributeName,\n                                    oldValue: mutation.oldValue,\n                                    value,\n                                },\n                                { forNewStep }\n                            );\n                        }\n                        this.setAttribute(node, mutation.attributeName, value);\n                    }\n                    break;\n                }\n                case \"remove\": {\n                    const toremove = this.idToNodeMap.get(mutation.id);\n                    if (toremove) {\n                        toremove.remove();\n                    }\n                    break;\n                }\n                case \"add\": {\n                    const node =\n                        this.idToNodeMap.get(mutation.id) || this.unserializeNode(mutation.node);\n                    if (!node) {\n                        continue;\n                    }\n\n                    this.setNodeId(node);\n\n                    if (mutation.append && this.idToNodeMap.get(mutation.append)) {\n                        this.idToNodeMap.get(mutation.append).append(node);\n                    } else if (mutation.before && this.idToNodeMap.get(mutation.before)) {\n                        this.idToNodeMap.get(mutation.before).before(node);\n                    } else if (mutation.after && this.idToNodeMap.get(mutation.after)) {\n                        this.idToNodeMap.get(mutation.after).after(node);\n                    } else {\n                        continue;\n                    }\n                    break;\n                }\n            }\n        }\n    }\n    /**\n     * @param { HistoryMutation[] } mutations\n     * @param { Object } options\n     * @param { boolean } options.forNewStep whether the mutations will be used\n     *        to create a new step\n     */\n    revertMutations(mutations, { forNewStep = false } = {}) {\n        for (const mutation of mutations.toReversed()) {\n            switch (mutation.type) {\n                case \"characterData\": {\n                    const node = this.idToNodeMap.get(mutation.id);\n                    if (node) {\n                        node.textContent = mutation.oldValue;\n                    }\n                    break;\n                }\n                case \"attributes\": {\n                    const node = this.idToNodeMap.get(mutation.id);\n                    if (node) {\n                        let value = this.getAttributeValue(\n                            mutation.attributeName,\n                            mutation.oldValue\n                        );\n                        for (const cb of this.getResource(\"attribute_change_processors\")) {\n                            value = cb(\n                                {\n                                    target: node,\n                                    attributeName: mutation.attributeName,\n                                    oldValue: mutation.value,\n                                    value,\n                                    reverse: true,\n                                },\n                                { forNewStep }\n                            );\n                        }\n                        this.setAttribute(node, mutation.attributeName, value);\n                    }\n                    break;\n                }\n                case \"remove\": {\n                    let nodeToRemove = this.idToNodeMap.get(mutation.id);\n                    if (!nodeToRemove) {\n                        nodeToRemove = this.unserializeNode(mutation.node);\n                        if (!nodeToRemove) {\n                            continue;\n                        }\n                    }\n                    if (mutation.nextId && this.idToNodeMap.get(mutation.nextId)?.isConnected) {\n                        const node = this.idToNodeMap.get(mutation.nextId);\n                        node && node.before(nodeToRemove);\n                    } else if (\n                        mutation.previousId &&\n                        this.idToNodeMap.get(mutation.previousId)?.isConnected\n                    ) {\n                        const node = this.idToNodeMap.get(mutation.previousId);\n                        node && node.after(nodeToRemove);\n                    } else {\n                        const node = this.idToNodeMap.get(mutation.parentId);\n                        node && node.append(nodeToRemove);\n                    }\n                    break;\n                }\n                case \"add\": {\n                    const node = this.idToNodeMap.get(mutation.id);\n                    if (node) {\n                        node.remove();\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Serialize an editor selection.\n     * @param { EditorSelection } selection\n     * @returns { SerializedSelection }\n     */\n    serializeSelection(selection) {\n        return {\n            anchorNodeId: this.nodeToIdMap.get(selection.anchorNode),\n            anchorOffset: selection.anchorOffset,\n            focusNodeId: this.nodeToIdMap.get(selection.focusNode),\n            focusOffset: selection.focusOffset,\n        };\n    }\n    /**\n     * Returns the deepest common ancestor element of the given mutations.\n     * @param {HistoryMutation[]} - The array of mutations.\n     * @returns {HTMLElement|null} - The common ancestor element.\n     */\n    getMutationsRoot(mutations) {\n        const nodes = mutations\n            .map((m) => this.idToNodeMap.get(m.parentId || m.id))\n            .filter((node) => this.editable.contains(node));\n        let commonAncestor = getCommonAncestor(nodes, this.editable);\n        if (commonAncestor?.nodeType === Node.TEXT_NODE) {\n            commonAncestor = commonAncestor.parentElement;\n        }\n        return commonAncestor;\n    }\n    /**\n     * Returns a function that can be later called to revert history to the\n     * current state.\n     * @returns {Function}\n     */\n    makeSavePoint() {\n        this.handleObserverRecords();\n        const draftMutations = this.currentStep.mutations.slice();\n        const step = this.steps.at(-1);\n        let applied = false;\n        // TODO ABD TODO @phoenix: selection may become obsolete, it should evolve with mutations.\n        const selectionToRestore = this.dependencies.selection.preserveSelection();\n        return () => {\n            if (applied) {\n                return;\n            }\n            applied = true;\n            const stepIndex = this.steps.findLastIndex((item) => item === step);\n            this.revertStepsUntil(stepIndex);\n            // Apply draft mutations to recover the same currentStep state\n            // as before.\n            this.applyMutations(draftMutations, { forNewStep: true });\n            this.handleObserverRecords();\n            // TODO ABD TODO @phoenix: evaluate if the selection is not restorable at the desired position\n            selectionToRestore.restore();\n            this.dispatchTo(\"restore_savepoint_handlers\");\n        };\n    }\n    /**\n     * Creates a set of functions to preview, apply, and revert an operation.\n     * @param {Function} operation\n     * @returns {PreviewableOperation}\n     */\n    makePreviewableOperation(operation) {\n        let revertOperation = () => {};\n\n        return {\n            preview: (...args) => {\n                revertOperation();\n                revertOperation = this.makeSavePoint();\n                operation(...args);\n            },\n            commit: (...args) => {\n                revertOperation();\n                operation(...args);\n                this.addStep();\n            },\n            revert: () => {\n                revertOperation();\n            },\n        };\n    }\n    /**\n     * Discard the current draft, and, if necessary, consume and revert\n     * reversible steps until the specified step index, and ensure that\n     * irreversible steps are maintained. This will add a new consumed step.\n     *\n     * @param {Number} stepIndex\n     */\n    revertStepsUntil(stepIndex) {\n        // Discard current draft.\n        this.handleObserverRecords();\n        this.revertMutations(this.currentStep.mutations);\n        this.observer.takeRecords();\n        this.currentStep.mutations = [];\n        let lastRevertedStep = this.currentStep;\n\n        if (stepIndex === this.steps.length - 1) {\n            return;\n        }\n        // Revert all mutations until stepIndex, and consume all reversible\n        // steps in the process (typically current peer steps).\n        for (let i = this.steps.length - 1; i > stepIndex; i--) {\n            const currentStep = this.steps[i];\n            this.revertMutations(currentStep.mutations, { forNewStep: true });\n            // Process (filter, handle and stage) mutations so that the\n            // attribute comparison for the state change is done with the\n            // intermediate attribute value and not with the final value in the\n            // DOM after all steps were reverted then applied again.\n            this.processNewRecords(this.observer.takeRecords());\n            if (this.isReversibleStep(i)) {\n                this.stepsStates.set(currentStep.id, \"consumed\");\n                lastRevertedStep = currentStep;\n            }\n        }\n        // Re-apply every non reversible steps (typically collaborators steps).\n        for (let i = stepIndex + 1; i < this.steps.length; i++) {\n            const currentStep = this.steps[i];\n            if (!this.isReversibleStep(i)) {\n                this.applyMutations(currentStep.mutations, { forNewStep: true });\n                this.processNewRecords(this.observer.takeRecords());\n            }\n        }\n        // TODO ABD TODO @phoenix: review selections, this selection could be obsolete\n        // depending on the non-reversible steps that were applied.\n        this.setSerializedSelection(lastRevertedStep.selection);\n        // Register resulting mutations as a new consumed step (prevent undo).\n        this.dispatchContentUpdated();\n        this.addStep({ stepState: \"consumed\" });\n    }\n\n    /**\n     * @param { string } attributeName\n     * @param { string } value\n     */\n    getAttributeValue(attributeName, value) {\n        if (typeof value === \"string\" && attributeName === \"class\") {\n            value = value\n                .split(\" \")\n                .filter((c) => !this.mutationFilteredClasses.has(c))\n                .join(\" \");\n        }\n        return value;\n    }\n    /**\n     * @param { Node } node\n     * @param { string } attributeName\n     * @param { string } attributeValue\n     */\n    setAttribute(node, attributeName, attributeValue) {\n        if (this.delegateTo(\"set_attribute_overrides\", node, attributeName, attributeValue)) {\n            return;\n        }\n\n        // if attributeValue is falsy but not null, we still need to apply it\n        if (attributeValue !== null) {\n            node.setAttribute(attributeName, attributeValue);\n        } else {\n            node.removeAttribute(attributeName);\n        }\n    }\n    /**\n     * Serialize a node and its children if the collaboration is true.\n     * @param { Node } node\n     * @param { Set<Node> } nodesToStripFromChildren\n     */\n    serializeNode(node, mutatedNodes) {\n        return this._serializeNode(node, mutatedNodes, this.nodeToIdMap);\n    }\n    /**\n     * Unserialize a node and its children if the collaboration is true.\n     *\n     * TODO: find a solution so that the following issue can never happen:\n     *   If there is already another node in `nodeToIdMap` pointing to the\n     *   current id before executing `this.nodeToIdMap.set(node, id)` in this\n     *   function, there will be 2 different nodes pointing to the same id.\n     *\n     *   2 different nodes for the same id is pretty common:\n     *     Unserializing a text node in `_unserializeNode` always creates\n     *     another (new) node.\n     *\n     *   If mutations concerning both nodes are bundled in the same step, they\n     *   will all be erroneously serialized as if they concern the node which\n     *   had its id set the latest, which is likely to cause issues when\n     *   applying these mutations (undo/redo, collaboration).\n     *\n     * @param { SerializedNode } node\n     * @returns { Node }\n     */\n    unserializeNode(node) {\n        let [unserializedNode, nodeMap] = this._unserializeNode(node, this.idToNodeMap);\n        const fakeNode = this.document.createElement(\"fake-el\");\n        fakeNode.appendChild(unserializedNode);\n        this.dependencies.sanitize.sanitize(fakeNode, { IN_PLACE: true });\n        unserializedNode = fakeNode.firstChild;\n\n        if (unserializedNode) {\n            // Only assing id to the remaining nodes, otherwise the removed\n            // nodes will still be accessible through the idToNodeMap and could\n            // lead to security issues.\n            for (const node of [unserializedNode, ...descendants(unserializedNode)]) {\n                const id = nodeMap.get(node);\n                if (id) {\n                    this.nodeToIdMap.set(node, id);\n                    this.idToNodeMap.set(id, node);\n                }\n            }\n            this.setNodeId(unserializedNode);\n            return unserializedNode;\n        }\n    }\n    /**\n     * Serialize a node and its children.\n     * @param { Node } node\n     * @param { [Set<Node>] } nodesToStripFromChildren\n     * @returns { SerializedNode }\n     */\n    _serializeNode(node, nodesToStripFromChildren = new Set()) {\n        const nodeId = this.nodeToIdMap.get(node);\n        if (!nodeId) {\n            return;\n        }\n        const result = {\n            nodeType: node.nodeType,\n            nodeId: nodeId,\n        };\n        if (node.nodeType === Node.TEXT_NODE) {\n            result.textValue = node.nodeValue;\n        } else if (node.nodeType === Node.ELEMENT_NODE) {\n            let childrenToSerialize = childNodes(node);\n            for (const cb of this.getResource(\"serializable_descendants_processors\")) {\n                childrenToSerialize = cb(node, childrenToSerialize);\n            }\n            result.tagName = node.tagName;\n            result.children = [];\n            result.attributes = {};\n            for (let i = 0; i < node.attributes.length; i++) {\n                result.attributes[node.attributes[i].name] = node.attributes[i].value;\n            }\n            for (const child of childrenToSerialize) {\n                if (!nodesToStripFromChildren.has(child.nodeId)) {\n                    const serializedChild = this._serializeNode(child, nodesToStripFromChildren);\n                    if (serializedChild) {\n                        result.children.push(serializedChild);\n                    }\n                }\n            }\n        }\n        return result;\n    }\n    /**\n     * Unserialize a node and its children.\n     * @param { SerializedNode } serializedNode\n     * @param { Map<Node, number> } _map\n     * @returns { Node, Map<Node, number> }\n     */\n    _unserializeNode(serializedNode, idToNodeMap = new Map(), _map = new Map()) {\n        let node = undefined;\n        if (serializedNode.nodeType === Node.TEXT_NODE) {\n            node = this.document.createTextNode(serializedNode.textValue);\n        } else if (serializedNode.nodeType === Node.ELEMENT_NODE) {\n            node = idToNodeMap.get(serializedNode.nodeId);\n            if (node) {\n                return [node, _map];\n            }\n            node = this.document.createElement(serializedNode.tagName);\n            for (const key in serializedNode.attributes) {\n                node.setAttribute(key, serializedNode.attributes[key]);\n            }\n            serializedNode.children.forEach((child) =>\n                node.append(this._unserializeNode(child, idToNodeMap, _map)[0])\n            );\n        } else {\n            console.warn(\"unknown node type\");\n        }\n        _map.set(node, serializedNode.nodeId);\n        return [node, _map];\n    }\n\n    _onDocumentBeforeInput(ev) {\n        if (this.editable.contains(ev.targget)) {\n            return;\n        }\n        if ([\"historyUndo\", \"historyRedo\"].includes(ev.inputType)) {\n            this._onKeyupResetContenteditableNodes.push(\n                ...this.editable.querySelectorAll(\"[contenteditable=true]\")\n            );\n            if (this.editable.getAttribute(\"contenteditable\") === \"true\") {\n                this._onKeyupResetContenteditableNodes.push(this.editable);\n            }\n\n            for (const node of this._onKeyupResetContenteditableNodes) {\n                node.setAttribute(\"contenteditable\", false);\n            }\n        }\n    }\n\n    _onDocumentInput(ev) {\n        if (\n            [\"historyUndo\", \"historyRedo\"].includes(ev.inputType) &&\n            this._onKeyupResetContenteditableNodes.length\n        ) {\n            for (const node of this._onKeyupResetContenteditableNodes) {\n                node.setAttribute(\"contenteditable\", true);\n            }\n            this._onKeyupResetContenteditableNodes = [];\n        }\n    }\n}\n", "import { Plugin } from \"../plugin\";\n\nexport class InputPlugin extends Plugin {\n    static id = \"input\";\n    static dependencies = [\"history\"];\n    setup() {\n        this.addDomListener(this.editable, \"beforeinput\", this.onBeforeInput);\n        this.addDomListener(this.editable, \"input\", this.onInput);\n    }\n\n    onBeforeInput(ev) {\n        this.dependencies.history.stageSelection();\n        this.dispatchTo(\"beforeinput_handlers\", ev);\n    }\n\n    onInput(ev) {\n        this.dependencies.history.addStep();\n        this.dispatchTo(\"input_handlers\", ev);\n    }\n}\n", "import { splitTextNode } from \"@html_editor/utils/dom\";\nimport { Plugin } from \"../plugin\";\nimport { CTGROUPS, CTYPES } from \"../utils/content_types\";\nimport { getState, isFakeLineBreak, prepareUpdate } from \"../utils/dom_state\";\nimport { DIRECTIONS, leftPos, rightPos } from \"../utils/position\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\n\n/**\n * @typedef { Object } LineBreakShared\n * @property { LineBreakPlugin['insertLineBreak'] } insertLineBreak\n * @property { LineBreakPlugin['insertLineBreakElement'] } insertLineBreakElement\n * @property { LineBreakPlugin['insertLineBreakNode'] } insertLineBreakNode\n */\n\nexport class LineBreakPlugin extends Plugin {\n    static dependencies = [\"selection\", \"history\", \"input\", \"delete\"];\n    static id = \"lineBreak\";\n    static shared = [\"insertLineBreak\", \"insertLineBreakNode\", \"insertLineBreakElement\"];\n    resources = {\n        beforeinput_handlers: this.onBeforeInput.bind(this),\n    };\n\n    insertLineBreak() {\n        this.dispatchTo(\"before_line_break_handlers\");\n        let selection = this.dependencies.selection.getEditableSelection();\n        if (!selection.isCollapsed) {\n            // @todo @phoenix collapseIfZWS is not tested\n            // this.shared.collapseIfZWS();\n            this.dependencies.delete.deleteSelection();\n            selection = this.dependencies.selection.getEditableSelection();\n        }\n\n        const targetNode = selection.anchorNode;\n        const targetOffset = selection.anchorOffset;\n\n        this.insertLineBreakNode({ targetNode, targetOffset });\n        this.dependencies.history.addStep();\n    }\n\n    /**\n     * @param {Object} params\n     * @param {Node} params.targetNode\n     * @param {number} params.targetOffset\n     */\n    insertLineBreakNode({ targetNode, targetOffset }) {\n        const closestEl = closestElement(targetNode);\n        if (closestEl && !closestEl.isContentEditable) {\n            return;\n        }\n        if (targetNode.nodeType === Node.TEXT_NODE) {\n            targetOffset = splitTextNode(targetNode, targetOffset);\n            targetNode = targetNode.parentElement;\n        }\n\n        if (this.delegateTo(\"insert_line_break_element_overrides\", { targetNode, targetOffset })) {\n            return;\n        }\n\n        this.insertLineBreakElement({ targetNode, targetOffset });\n    }\n\n    /**\n     * @param {Object} params\n     * @param {HTMLElement} params.targetNode\n     * @param {number} params.targetOffset\n     */\n    insertLineBreakElement({ targetNode, targetOffset }) {\n        const closestEl = closestElement(targetNode);\n        if (closestEl && !closestEl.isContentEditable) {\n            return;\n        }\n        const restore = prepareUpdate(targetNode, targetOffset);\n\n        const brEl = this.document.createElement(\"br\");\n        const brEls = [brEl];\n        if (targetOffset >= targetNode.childNodes.length) {\n            targetNode.appendChild(brEl);\n        } else {\n            targetNode.insertBefore(brEl, targetNode.childNodes[targetOffset]);\n        }\n        if (\n            isFakeLineBreak(brEl) &&\n            !(getState(...leftPos(brEl), DIRECTIONS.LEFT).cType & (CTGROUPS.BLOCK | CTYPES.BR))\n        ) {\n            const brEl2 = this.document.createElement(\"br\");\n            brEl.before(brEl2);\n            brEls.unshift(brEl2);\n        }\n\n        restore();\n\n        // @todo ask AGE about why this code was only needed for unbreakable.\n        // See `this._applyCommand('oEnter') === UNBREAKABLE_ROLLBACK_CODE` in\n        // web_editor. Because now we should have a strong handling of the link\n        // selection with the link isolation, if we want to insert a BR outside,\n        // we can move the cursor outside the link.\n        // So if there is no reason to keep this code, we should remove it.\n        //\n        // const anchor = brEls[0].parentElement;\n        // // @todo @phoenix should this case be handled by a LinkPlugin?\n        // // @todo @phoenix Don't we want this for all spans ?\n        // if (anchor.nodeName === \"A\" && brEls.includes(anchor.firstChild)) {\n        //     brEls.forEach((br) => anchor.before(br));\n        //     const pos = rightPos(brEls[brEls.length - 1]);\n        //     this.dependencies.selection.setSelection({ anchorNode: pos[0], anchorOffset: pos[1] });\n        // } else if (anchor.nodeName === \"A\" && brEls.includes(anchor.lastChild)) {\n        //     brEls.forEach((br) => anchor.after(br));\n        //     const pos = rightPos(brEls[0]);\n        //     this.dependencies.selection.setSelection({ anchorNode: pos[0], anchorOffset: pos[1] });\n        // }\n        for (const el of brEls) {\n            // @todo @phoenix we don t want to setSelection multiple times\n            if (el.parentNode) {\n                const pos = rightPos(el);\n                this.dependencies.selection.setSelection({\n                    anchorNode: pos[0],\n                    anchorOffset: pos[1],\n                });\n                break;\n            }\n        }\n    }\n\n    onBeforeInput(e) {\n        if (e.inputType === \"insertLineBreak\") {\n            e.preventDefault();\n            this.insertLineBreak();\n        }\n    }\n}\n", "import { getDeepestPosition, isParagraphRelatedElement } from \"@html_editor/utils/dom_info\";\nimport { Plugin } from \"../plugin\";\nimport { isNotAllowedContent } from \"./selection_plugin\";\nimport { nodeSize } from \"@html_editor/utils/position\";\n\nexport class NoInlineRootPlugin extends Plugin {\n    static id = \"noInlineRoot\";\n    static dependencies = [\"baseContainer\", \"selection\", \"history\"];\n\n    resources = {\n        ...(!this.config.allowInlineAtRoot && {\n            fix_selection_on_editable_root_handlers: this.fixSelectionOnEditableRoot.bind(this),\n        }),\n    };\n\n    setup() {\n        this.addDomListener(this.editable, \"keydown\", (ev) => {\n            this.currentKeyDown = ev.key;\n        });\n        this.addDomListener(this.editable, \"pointerdown\", () => {\n            this.isPointerDown = true;\n        });\n        this.addDomListener(this.editable, \"pointerup\", () => {\n            this.isPointerDown = false;\n            this.preventNextPointerdownFix = false;\n        });\n    }\n\n    /**\n     * Places the cursor in a safe place (not the editable root).\n     * Inserts an empty paragraph if selection results from mouse click and\n     * there's no other way to insert text before/after a block.\n     *\n     * @param {Selection} selection - Collapsed selection at the editable root.\n     */\n    fixSelectionOnEditableRoot(selection) {\n        if (\n            !selection.isCollapsed ||\n            selection.anchorNode !== this.editable ||\n            this.config.allowInlineAtRoot\n        ) {\n            return false;\n        }\n\n        const nodeAfterCursor = this.editable.childNodes[selection.anchorOffset];\n        const nodeBeforeCursor = nodeAfterCursor && nodeAfterCursor.previousElementSibling;\n\n        return (\n            this.fixSelectionOnEditableRootArrowKeys(nodeAfterCursor, nodeBeforeCursor) ||\n            this.fixSelectionOnEditableRootGeneric(nodeAfterCursor, nodeBeforeCursor) ||\n            this.fixSelectionOnEditableRootCreateP(nodeAfterCursor, nodeBeforeCursor)\n        );\n    }\n    /**\n     * @param {Node} nodeAfterCursor\n     * @param {Node} nodeBeforeCursor\n     * @returns {boolean}\n     */\n    fixSelectionOnEditableRootArrowKeys(nodeAfterCursor, nodeBeforeCursor) {\n        const currentKeyDown = this.currentKeyDown;\n        delete this.currentKeyDown;\n        if (currentKeyDown === \"ArrowRight\" || currentKeyDown === \"ArrowDown\") {\n            while (nodeAfterCursor && isNotAllowedContent(nodeAfterCursor)) {\n                nodeAfterCursor = nodeAfterCursor.nextElementSibling;\n            }\n            const [anchorNode] = getDeepestPosition(nodeAfterCursor, 0);\n            if (nodeAfterCursor) {\n                this.dependencies.selection.setSelection({\n                    anchorNode: anchorNode,\n                    anchorOffset: 0,\n                });\n                return true;\n            } else {\n                this.dependencies.selection.resetActiveSelection();\n            }\n        } else if (currentKeyDown === \"ArrowLeft\" || currentKeyDown === \"ArrowUp\") {\n            while (nodeBeforeCursor && isNotAllowedContent(nodeBeforeCursor)) {\n                nodeBeforeCursor = nodeBeforeCursor.previousElementSibling;\n            }\n            if (nodeBeforeCursor) {\n                const [anchorNode, anchorOffset] = getDeepestPosition(\n                    nodeBeforeCursor,\n                    nodeSize(nodeBeforeCursor)\n                );\n                this.dependencies.selection.setSelection({\n                    anchorNode: anchorNode,\n                    anchorOffset: anchorOffset,\n                });\n                return true;\n            } else {\n                this.dependencies.selection.resetActiveSelection();\n            }\n        }\n    }\n    /**\n     * @param {Node} nodeAfterCursor\n     * @param {Node} nodeBeforeCursor\n     * @returns {boolean}\n     */\n    fixSelectionOnEditableRootGeneric(nodeAfterCursor, nodeBeforeCursor) {\n        // Handle arrow key presses.\n        if (nodeAfterCursor && isParagraphRelatedElement(nodeAfterCursor)) {\n            // Cursor is right before a 'P'.\n            this.dependencies.selection.setCursorStart(nodeAfterCursor);\n            return true;\n        } else if (nodeBeforeCursor && isParagraphRelatedElement(nodeBeforeCursor)) {\n            // Cursor is right after a 'P'.\n            this.dependencies.selection.setCursorEnd(nodeBeforeCursor);\n            return true;\n        }\n    }\n    /**\n     * Handle cursor not next to a 'P'.\n     * Insert a new 'P' if selection resulted from a mouse click.\n     *\n     * In some situations (notably around tables and horizontal\n     * separators), the cursor could be placed having its anchorNode at\n     * the editable root, allowing the user to insert inlined text at\n     * it.\n     *\n     * @param {Node} nodeAfterCursor\n     * @param {Node} nodeBeforeCursor\n     * @returns {boolean}\n     */\n    fixSelectionOnEditableRootCreateP(nodeAfterCursor, nodeBeforeCursor) {\n        if (this.isPointerDown && !this.preventNextPointerdownFix) {\n            // The setSelection at the end of this fix could trigger another\n            // setSelection (that would re-trigger this fix). So this flag is\n            // used to prevent to fix twice from the same mouse event.\n            this.preventNextPointerdownFix = true;\n\n            const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n            baseContainer.append(this.document.createElement(\"br\"));\n            if (!nodeAfterCursor) {\n                // Cursor is at the end of the editable.\n                this.editable.append(baseContainer);\n            } else if (!nodeBeforeCursor) {\n                // Cursor is at the beginning of the editable.\n                this.editable.prepend(baseContainer);\n            } else {\n                // Cursor is between two non-p blocks\n                nodeAfterCursor.before(baseContainer);\n            }\n            this.dependencies.selection.setCursorStart(baseContainer);\n            this.dependencies.history.addStep();\n            return true;\n        }\n        return false;\n    }\n}\n", "import { Component, onWillDestroy, useEffect, useExternalListener, useRef, xml } from \"@odoo/owl\";\nimport { usePosition } from \"@web/core/position/position_hook\";\nimport { useActiveElement } from \"@web/core/ui/ui_service\";\n\nexport class EditorOverlay extends Component {\n    static template = xml`\n        <div t-ref=\"root\" class=\"overlay\" t-att-class=\"props.className\" t-on-pointerdown.stop=\"() => {}\">\n            <t t-component=\"props.Component\" t-props=\"props.props\"/>\n        </div>`;\n\n    static props = {\n        target: { validate: (el) => el.nodeType === Node.ELEMENT_NODE, optional: true },\n        initialSelection: { type: Object, optional: true },\n        Component: Function,\n        props: { type: Object, optional: true },\n        editable: { validate: (el) => el.nodeType === Node.ELEMENT_NODE },\n        bus: Object,\n        getContainer: Function,\n        history: Object,\n        close: Function,\n        isOverlayOpen: Function,\n\n        // Props from createOverlay\n        positionOptions: { type: Object, optional: true },\n        className: { type: String, optional: true },\n        closeOnPointerdown: { type: Boolean, optional: true },\n        hasAutofocus: { type: Boolean, optional: true },\n    };\n\n    static defaultProps = {\n        className: \"\",\n        closeOnPointerdown: true,\n        hasAutofocus: false,\n    };\n\n    setup() {\n        this.lastSelection = this.props.initialSelection;\n        let getTarget, position;\n        if (this.props.target) {\n            getTarget = () => this.props.target;\n        } else {\n            useExternalListener(this.props.bus, \"updatePosition\", () => {\n                position.unlock();\n            });\n            const editable = this.props.editable;\n            this.rangeElement = editable.ownerDocument.createElement(\"range-el\");\n            editable.after(this.rangeElement);\n            onWillDestroy(() => {\n                this.rangeElement.remove();\n            });\n            getTarget = this.getSelectionTarget.bind(this);\n        }\n\n        const rootRef = useRef(\"root\");\n\n        if (this.props.positionOptions?.updatePositionOnResize ?? true) {\n            const resizeObserver = new ResizeObserver(() => {\n                position.unlock();\n            });\n            useEffect(\n                (root) => {\n                    resizeObserver.observe(root);\n                    return () => {\n                        resizeObserver.unobserve(root);\n                    };\n                },\n                () => [rootRef.el]\n            );\n        }\n\n        if (this.props.closeOnPointerdown) {\n            const editableDocument = this.props.editable.ownerDocument;\n            useExternalListener(editableDocument, \"pointerdown\", this.props.close);\n            // Listen to pointerdown outside the iframe\n            if (editableDocument !== document) {\n                useExternalListener(document, \"pointerdown\", this.props.close);\n            }\n        }\n\n        if (this.props.hasAutofocus) {\n            useActiveElement(\"root\");\n        }\n        const positionOptions = {\n            position: \"bottom-start\",\n            container: this.props.getContainer,\n            ...this.props.positionOptions,\n            onPositioned: (el, solution) => {\n                this.props.positionOptions?.onPositioned?.(el, solution);\n                this.updateVisibility(el, solution);\n            },\n        };\n        position = usePosition(\"root\", getTarget, positionOptions);\n    }\n\n    getSelectionTarget() {\n        const doc = this.props.editable.ownerDocument;\n        const selection = doc.getSelection();\n        if (!selection || !selection.rangeCount || !this.props.isOverlayOpen()) {\n            return null;\n        }\n        const inEditable = this.props.editable.contains(selection.anchorNode);\n        let range;\n        if (inEditable) {\n            range = selection.getRangeAt(0);\n            this.lastSelection = { range };\n        } else {\n            if (!this.lastSelection) {\n                return null;\n            }\n            range = this.lastSelection.range;\n        }\n        let rect = range.getBoundingClientRect();\n        if (rect.x === 0 && rect.width === 0 && rect.height === 0) {\n            // Attention, using disableObserver and enableObserver is always dangerous (when we add or remove nodes)\n            // because if another mutation uses the target that is not observed, that mutation can never be applied\n            // again (when undo/redo and in collaboration).\n            this.props.history.disableObserver();\n            const clonedRange = range.cloneRange();\n            const shadowCaret = doc.createTextNode(\"|\");\n            clonedRange.insertNode(shadowCaret);\n            clonedRange.selectNode(shadowCaret);\n            rect = clonedRange.getBoundingClientRect();\n            shadowCaret.remove();\n            clonedRange.detach();\n            this.props.history.enableObserver();\n        }\n        // Html element with a patched getBoundingClientRect method. It\n        // represents the range as a (HTMLElement) target for the usePosition\n        // hook.\n        this.rangeElement.getBoundingClientRect = () => rect;\n        return this.rangeElement;\n    }\n\n    updateVisibility(overlayElement, solution) {\n        // @todo: mobile tests rely on a visible (yet overflowing) toolbar\n        // Remove this once the mobile toolbar is fixed?\n        if (this.env.isSmall) {\n            return;\n        }\n        const containerRect = this.props.getContainer().getBoundingClientRect();\n        overlayElement.style.visibility = solution.top > containerRect.top ? \"visible\" : \"hidden\";\n    }\n}\n", "import { markRaw, EventBus } from \"@odoo/owl\";\nimport { Plugin } from \"../plugin\";\nimport { EditorOverlay } from \"./overlay\";\nimport { throttleForAnimation } from \"@web/core/utils/timing\";\nimport { findUpTo } from \"@html_editor/utils/dom_traversal\";\n\n/**\n * @typedef { Object } OverlayShared\n * @property { OverlayPlugin['createOverlay'] } createOverlay\n */\n\n/**\n * Provides the following feature:\n * - adding a component in overlay above the editor, with proper positioning\n */\nexport class OverlayPlugin extends Plugin {\n    static id = \"overlay\";\n    static dependencies = [\"history\"];\n    static shared = [\"createOverlay\"];\n    resources = {\n        step_added_handlers: this.getScrollContainer.bind(this),\n    };\n\n    overlays = [];\n\n    setup() {\n        this.iframe = this.document.defaultView.frameElement;\n        this.topDocument = this.iframe?.ownerDocument || this.document;\n        this.container = this.getScrollContainer();\n        this.throttledUpdateContainer = throttleForAnimation(() => {\n            this.container = this.getScrollContainer();\n        });\n        this.addDomListener(this.topDocument.defaultView, \"resize\", this.throttledUpdateContainer);\n    }\n\n    destroy() {\n        this.throttledUpdateContainer.cancel();\n        super.destroy();\n        for (const overlay of this.overlays) {\n            overlay.close();\n        }\n    }\n\n    /**\n     * Creates an overlay component and adds it to the list of overlays.\n     *\n     * @param {Function} Component\n     * @param {Object} [props={}]\n     * @param {Object} [options]\n     * @returns {Overlay}\n     */\n    createOverlay(Component, props = {}, options) {\n        const overlay = new Overlay(this, Component, () => this.container, props, options);\n        this.overlays.push(overlay);\n        return overlay;\n    }\n\n    getScrollContainer() {\n        const isScrollable = (element) =>\n            element.scrollHeight > element.clientHeight &&\n            [\"auto\", \"scroll\"].includes(getComputedStyle(element).overflowY);\n\n        return (\n            findUpTo(this.iframe || this.editable, null, isScrollable) ||\n            this.topDocument.documentElement\n        );\n    }\n}\n\nexport class Overlay {\n    constructor(plugin, C, getContainer, props, options) {\n        this.plugin = plugin;\n        this.C = C;\n        this.editorOverlayProps = props;\n        this.options = options;\n        this.isOpen = false;\n        this._remove = null;\n        this.component = null;\n        this.bus = new EventBus();\n        this.getContainer = getContainer;\n    }\n\n    /**\n     * @param {Object} options\n     * @param {HTMLElement | null} [options.target] for the overlay.\n     *  If null or undefined, the current selection will be used instead\n     * @param {any} [options.props] overlay component props\n     */\n    open({ target, props }) {\n        if (this.isOpen) {\n            this.updatePosition();\n        } else {\n            this.isOpen = true;\n            const selection = this.plugin.editable.ownerDocument.getSelection();\n            let initialSelection;\n            if (selection && selection.type !== \"None\") {\n                initialSelection = {\n                    range: selection.getRangeAt(0),\n                };\n            }\n            this._remove = this.plugin.services.overlay.add(\n                EditorOverlay,\n                markRaw({\n                    ...this.editorOverlayProps,\n                    Component: this.C,\n                    editable: this.plugin.editable,\n                    props,\n                    target,\n                    initialSelection,\n                    bus: this.bus,\n                    getContainer: this.getContainer,\n                    close: this.close.bind(this),\n                    isOverlayOpen: this.isOverlayOpen.bind(this),\n                    history: {\n                        enableObserver: this.plugin.dependencies.history.enableObserver,\n                        disableObserver: this.plugin.dependencies.history.disableObserver,\n                    },\n                }),\n                {\n                    ...this.options,\n                }\n            );\n        }\n    }\n\n    close() {\n        this.isOpen = false;\n        if (this._remove) {\n            this._remove();\n        }\n    }\n\n    isOverlayOpen() {\n        return this.isOpen;\n    }\n\n    updatePosition() {\n        this.bus.trigger(\"updatePosition\");\n    }\n}\n", "import { Plugin } from \"../plugin\";\nimport { isProtecting, isUnprotecting } from \"../utils/dom_info\";\nimport { childNodes } from \"../utils/dom_traversal\";\n\nconst PROTECTED_SELECTOR = `[data-oe-protected=\"true\"],[data-oe-protected=\"\"]`;\nconst UNPROTECTED_SELECTOR = `[data-oe-protected=\"false\"]`;\n\n/**\n * @typedef { Object } ProtectedNodeShared\n * @property { ProtectedNodePlugin['setProtectingNode'] } setProtectingNode\n */\n\nexport class ProtectedNodePlugin extends Plugin {\n    static id = \"protectedNode\";\n    static shared = [\"setProtectingNode\"];\n    resources = {\n        /** Handlers */\n        clean_for_save_handlers: ({ root }) => this.cleanForSave(root),\n        normalize_handlers: this.normalize.bind(this),\n        before_filter_mutation_record_handlers: this.beforeFilteringMutationRecords.bind(this),\n\n        unsplittable_node_predicates: [\n            isProtecting, // avoid merge\n            isUnprotecting,\n        ],\n        savable_mutation_record_predicates: this.isMutationRecordSavable.bind(this),\n        removable_descendants_providers: this.filterDescendantsToRemove.bind(this),\n    };\n\n    setup() {\n        this.protectedNodes = new WeakSet();\n    }\n\n    filterDescendantsToRemove(elem) {\n        // TODO @phoenix: history plugin can register protected nodes in its\n        // id maps, should it be prevented? => if yes, take care that data-oe-protected=\"false\"\n        // elements should also be registered even though they are protected.\n        if (isProtecting(elem)) {\n            const descendantsToRemove = [];\n            for (const candidate of elem.querySelectorAll(UNPROTECTED_SELECTOR)) {\n                if (candidate.closest(PROTECTED_SELECTOR) === elem) {\n                    descendantsToRemove.push(...childNodes(candidate));\n                }\n            }\n            return descendantsToRemove;\n        }\n    }\n\n    protectNode(node) {\n        if (node.nodeType === Node.ELEMENT_NODE) {\n            if (node.matches(UNPROTECTED_SELECTOR)) {\n                this.unProtectDescendants(node);\n            } else if (!this.protectedNodes.has(node)) {\n                this.protectDescendants(node);\n            }\n            // assume that descendants are already handled if the node\n            // is already protected.\n        }\n        this.protectedNodes.add(node);\n    }\n\n    unProtectNode(node) {\n        if (node.nodeType === Node.ELEMENT_NODE) {\n            if (node.matches(PROTECTED_SELECTOR)) {\n                this.protectDescendants(node);\n            } else if (this.protectedNodes.has(node)) {\n                this.unProtectDescendants(node);\n            }\n            // assume that descendants are already handled if the node\n            // is already not protected.\n        }\n        this.protectedNodes.delete(node);\n    }\n\n    protectDescendants(node) {\n        let child = node.firstChild;\n        while (child) {\n            this.protectNode(child);\n            child = child.nextSibling;\n        }\n    }\n\n    unProtectDescendants(node) {\n        let child = node.firstChild;\n        while (child) {\n            this.unProtectNode(child);\n            child = child.nextSibling;\n        }\n    }\n\n    beforeFilteringMutationRecords(records) {\n        for (const record of records) {\n            if (record.type === \"childList\") {\n                if (record.target.nodeType !== Node.ELEMENT_NODE) {\n                    return;\n                }\n                if (\n                    (this.protectedNodes.has(record.target) &&\n                        !record.target.matches(UNPROTECTED_SELECTOR)) ||\n                    record.target.matches(PROTECTED_SELECTOR)\n                ) {\n                    for (const addedNode of record.addedNodes) {\n                        this.protectNode(addedNode);\n                    }\n                } else if (\n                    !this.protectedNodes.has(record.target) ||\n                    record.target.matches(UNPROTECTED_SELECTOR)\n                ) {\n                    for (const addedNode of record.addedNodes) {\n                        this.unProtectNode(addedNode);\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * @param {MutationRecord} record\n     * @return {boolean}\n     */\n    isMutationRecordSavable(record) {\n        if (record.type === \"attributes\") {\n            if (record.attributeName === \"contenteditable\") {\n                return (\n                    !this.protectedNodes.has(record.target) ||\n                    record.target.matches(UNPROTECTED_SELECTOR)\n                );\n            }\n        } else if (record.target.nodeType === Node.ELEMENT_NODE) {\n            return !(\n                (this.protectedNodes.has(record.target) &&\n                    !record.target.matches(UNPROTECTED_SELECTOR)) ||\n                record.target.matches(PROTECTED_SELECTOR)\n            );\n        }\n        return !this.protectedNodes.has(record.target);\n    }\n\n    forEachProtectingElem(elem, callback) {\n        const selector = `[data-oe-protected]`;\n        const protectingNodes = [...elem.querySelectorAll(selector)].reverse();\n        if (elem.matches(selector)) {\n            protectingNodes.push(elem);\n        }\n        for (const protectingNode of protectingNodes) {\n            if (protectingNode.dataset.oeProtected === \"false\") {\n                callback(protectingNode, false);\n            } else {\n                callback(protectingNode, true);\n            }\n        }\n    }\n\n    normalize(elem) {\n        this.forEachProtectingElem(elem, this.setProtectingNode.bind(this));\n    }\n\n    setProtectingNode(elem, protecting) {\n        elem.dataset.oeProtected = protecting;\n        // contenteditable attribute is set on (un)protecting nodes for\n        // implementation convenience. This could be removed but the editor\n        // should be adapted to handle some use cases that are handled for\n        // contenteditable elements. Currently unsupported configurations:\n        // 1) unprotected non-editable content: would typically be added/removed\n        // programmatically and shared in collaboration => some logic should\n        // be added to handle undo/redo properly for consistency.\n        // -> A adds content, A replaces his content with a new one, B replaces\n        //   content of A with his own, A undo => there is now the content of B\n        //   and the old content of A in the node, is it still coherent?\n        // 2) protected editable content: need a specification of which\n        // functions of the editor are allowed to work (and how) in that\n        // editable part (none?) => should be enforced.\n        if (protecting) {\n            elem.setAttribute(\"contenteditable\", \"false\");\n            this.protectDescendants(elem);\n        } else {\n            elem.setAttribute(\"contenteditable\", \"true\");\n            this.unProtectDescendants(elem);\n        }\n    }\n\n    cleanForSave(clone) {\n        this.forEachProtectingElem(clone, (protectingNode) => {\n            protectingNode.removeAttribute(\"contenteditable\");\n        });\n    }\n}\n", "import { selectElements } from \"@html_editor/utils/dom_traversal\";\nimport { Plugin } from \"../plugin\";\n\n/**\n * @typedef { Object } SanitizeShared\n * @property { SanitizePlugin['sanitize'] } sanitize\n */\n\nexport class SanitizePlugin extends Plugin {\n    static id = \"sanitize\";\n    static shared = [\"sanitize\", \"restoreSanitizedContentEditable\"];\n    setup() {\n        if (!window.DOMPurify) {\n            throw new Error(\"DOMPurify is not available\");\n        }\n        this.DOMPurify = DOMPurify(this.document.defaultView);\n    }\n    /**\n     * Sanitizes in place an html element. Current implementation uses the\n     * DOMPurify library.\n     *\n     * @param {HTMLElement} elem\n     * @returns {HTMLElement} the element itself\n     */\n    sanitize(elem) {\n        return this.DOMPurify.sanitize(elem, {\n            IN_PLACE: true,\n            ADD_TAGS: [\"#document-fragment\", \"fake-el\"],\n            ADD_ATTR: [\"contenteditable\"],\n        });\n    }\n\n    restoreSanitizedContentEditable(root) {\n        for (const node of selectElements(root, \".o_not_editable, .o_editable\")) {\n            node.contentEditable = node.matches(\".o_editable\");\n        }\n    }\n}\n", "import { closestBlock } from \"@html_editor/utils/blocks\";\nimport {\n    getDeepestPosition,\n    isMediaElement,\n    isProtected,\n    isProtecting,\n    isUnprotecting,\n    previousLeaf,\n} from \"@html_editor/utils/dom_info\";\nimport {\n    childNodes,\n    closestElement,\n    descendants,\n    firstLeaf,\n    lastLeaf,\n} from \"@html_editor/utils/dom_traversal\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { Plugin } from \"../plugin\";\nimport { DIRECTIONS, endPos, leftPos, nodeSize, rightPos } from \"../utils/position\";\nimport {\n    getAdjacentCharacter,\n    normalizeCursorPosition,\n    normalizeDeepCursorPosition,\n    normalizeFakeBR,\n} from \"../utils/selection\";\nimport { scrollTo } from \"@web/core/utils/scrolling\";\n\n/**\n * @typedef { Object } EditorSelection\n * @property { Node } anchorNode\n * @property { number } anchorOffset\n * @property { Node } focusNode\n * @property { number } focusOffset\n * @property { Node } startContainer\n * @property { number } startOffset\n * @property { Node } endContainer\n * @property { number } endOffset\n * @property { Node } commonAncestorContainer\n * @property { boolean } isCollapsed\n * @property { boolean } direction\n */\n\n/**\n * @typedef {Object} SelectionData\n * @property {EditorSelection} documentSelection\n * @property {EditorSelection} editableSelection\n * @property {EditorSelection} deepEditableSelection\n * @property { boolean } documentSelectionIsInEditable\n * @property { boolean } documentSelectionIsProtected\n * @property { boolean } documentSelectionIsProtecting\n */\n\n/**\n * @typedef {Object} Cursors\n * @property {() => void} restore\n * @property {(callback: (cursor: Cursor) => void) => Cursors} update\n * @property {(node: Node, newNode: Node) => Cursors} remapNode\n * @property {(node: Node, newOffset: number) => Cursors} setOffset\n * @property {(node: Node, shiftOffset: number) => Cursors} shiftOffset\n */\n\n/**\n * @typedef {Object} Cursor\n * @property {Node} node\n * @property {number} offset\n */\n\n// https://developer.mozilla.org/en-US/docs/Glossary/Void_element\nconst VOID_ELEMENT_NAMES = [\n    \"AREA\",\n    \"BASE\",\n    \"BR\",\n    \"COL\",\n    \"EMBED\",\n    \"HR\",\n    \"IMG\",\n    \"INPUT\",\n    \"KEYGEN\",\n    \"LINK\",\n    \"META\",\n    \"PARAM\",\n    \"SOURCE\",\n    \"TRACK\",\n    \"WBR\",\n];\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\n/**\n * @returns edges nodes if they do not have content selected\n */\nfunction getUnselectedEdgeNodes(selection) {\n    const startEdgeNodes = (node, offset) =>\n        node === selection.commonAncestorContainer || offset < nodeSize(node)\n            ? []\n            : [node, ...startEdgeNodes(...rightPos(node))];\n    const endEdgeNodes = (node, offset) =>\n        node === selection.commonAncestorContainer || offset > 0\n            ? []\n            : [node, ...endEdgeNodes(...leftPos(node))];\n    return new Set([\n        ...startEdgeNodes(selection.startContainer, selection.startOffset),\n        ...endEdgeNodes(selection.endContainer, selection.endOffset),\n    ]);\n}\n\n/**\n * Check if an element is in the viewport.\n *\n * @param {HTMLElement} element - The element to check.\n * @param {Object} [options] - Options for the observer.\n * @param {number} [options.threshold=0.5] - The intersection observer threshold.\n * @returns {Promise<boolean>} - A promise that resolves with a boolean indicating whether the element is in the viewport.\n */\nfunction isElementInViewport(element, options = { threshold: 0.5 }) {\n    return new Promise((resolve) => {\n        const observer = new IntersectionObserver(([entry]) => {\n            resolve(entry.isIntersecting);\n            observer.disconnect();\n        }, options);\n        observer.observe(element);\n    });\n}\n\n/**\n * @typedef { Object } SelectionShared\n * @property { SelectionPlugin['extractContent'] } extractContent\n * @property { SelectionPlugin['focusEditable'] } focusEditable\n * @property { SelectionPlugin['getEditableSelection'] } getEditableSelection\n * @property { SelectionPlugin['getSelectedNodes'] } getSelectedNodes\n * @property { SelectionPlugin['getSelectionData'] } getSelectionData\n * @property { SelectionPlugin['getTraversedBlocks'] } getTraversedBlocks\n * @property { SelectionPlugin['getTraversedNodes'] } getTraversedNodes\n * @property { SelectionPlugin['modifySelection'] } modifySelection\n * @property { SelectionPlugin['preserveSelection'] } preserveSelection\n * @property { SelectionPlugin['rectifySelection'] } rectifySelection\n * @property { SelectionPlugin['resetActiveSelection'] } resetActiveSelection\n * @property { SelectionPlugin['resetSelection'] } resetSelection\n * @property { SelectionPlugin['setCursorEnd'] } setCursorEnd\n * @property { SelectionPlugin['setCursorStart'] } setCursorStart\n * @property { SelectionPlugin['setSelection'] } setSelection\n */\n\nexport class SelectionPlugin extends Plugin {\n    static id = \"selection\";\n    static shared = [\n        \"getSelectionData\",\n        \"getEditableSelection\",\n        \"setSelection\",\n        \"setCursorStart\",\n        \"setCursorEnd\",\n        \"extractContent\",\n        \"preserveSelection\",\n        \"resetSelection\",\n        \"getSelectedNodes\",\n        \"getTraversedNodes\",\n        \"getTraversedBlocks\",\n        \"modifySelection\",\n        \"rectifySelection\",\n        // todo: ideally, this should not be shared\n        \"resetActiveSelection\",\n        \"focusEditable\",\n        // \"collapseIfZWS\",\n        \"isSelectionInEditable\",\n    ];\n    resources = {\n        user_commands: { id: \"selectAll\", run: this.selectAll.bind(this) },\n        shortcuts: [{ hotkey: \"control+a\", commandId: \"selectAll\" }],\n    };\n\n    setup() {\n        this.resetSelection();\n        this.addDomListener(this.document, \"selectionchange\", async () => {\n            this.updateActiveSelection();\n            const selection = this.document.getSelection();\n            if (selection.isCollapsed && this.isSelectionInEditable(selection)) {\n                const element = closestElement(selection.focusNode);\n                const isInViewport = await isElementInViewport(element);\n                if (!isInViewport) {\n                    scrollTo(element);\n                }\n            }\n        });\n        this.addDomListener(this.editable, \"mousedown\", (ev) => {\n            if (ev.detail >= 3) {\n                this.correctTripleClick = true;\n            }\n            this.handleEmptySelection();\n        });\n        this.addDomListener(this.editable, \"keydown\", (ev) => {\n            const handled = [\n                \"arrowright\",\n                \"shift+arrowright\",\n                \"arrowleft\",\n                \"shift+arrowleft\",\n                \"shift+arrowup\",\n                \"shift+arrowdown\",\n            ];\n            if (handled.includes(getActiveHotkey(ev))) {\n                this.onKeyDownArrows(ev);\n            }\n        });\n    }\n\n    selectAll() {\n        const selection = this.getEditableSelection();\n        const containerSelector = \"#wrap > *, .oe_structure > *, [contenteditable]\";\n        const container = selection && closestElement(selection.anchorNode, containerSelector);\n        const [anchorNode, anchorOffset] = getDeepestPosition(container, 0);\n        const [focusNode, focusOffset] = getDeepestPosition(container, nodeSize(container));\n        this.setSelection({ anchorNode, anchorOffset, focusNode, focusOffset });\n    }\n\n    resetSelection() {\n        this.activeSelection = this.makeActiveSelection();\n    }\n\n    handleEmptySelection() {\n        const selection = this.getEditableSelection();\n        if (selection.anchorNode && !selection.isCollapsed) {\n            const [deepAnchorNode, deepAnchorOffset] = getDeepestPosition(\n                selection.anchorNode,\n                selection.anchorOffset\n            );\n            const [deepFocusNode, deepFocusOffset] = getDeepestPosition(\n                selection.focusNode,\n                selection.focusOffset\n            );\n\n            const range = new Range();\n            range.setStart(deepAnchorNode, deepAnchorOffset);\n            range.setEnd(deepFocusNode, deepFocusOffset);\n            const rangeContentChildNodes = range.cloneContents().childNodes;\n            if (\n                rangeContentChildNodes.length === 1 &&\n                rangeContentChildNodes[0].nodeName === \"BR\"\n            ) {\n                this.setSelection({\n                    anchorNode: deepAnchorNode,\n                    anchorOffset: 0,\n                    focusNode: deepAnchorNode,\n                    focusOffset: 0,\n                });\n            }\n        }\n    }\n\n    /**\n     * Update the active selection to the current selection in the editor.\n     */\n    updateActiveSelection() {\n        this.previousActiveSelection = this.activeSelection;\n        const selectionData = this.getSelectionData();\n        if (selectionData.documentSelectionIsInEditable) {\n            if (this.correctTripleClick) {\n                this.correctTripleClick = false;\n                let { anchorNode, anchorOffset, focusNode, focusOffset } = this.activeSelection;\n                if (focusOffset === 0 && anchorNode !== focusNode) {\n                    [focusNode, focusOffset] = endPos(previousLeaf(focusNode));\n                    return this.setSelection({ anchorNode, anchorOffset, focusNode, focusOffset });\n                }\n            }\n\n            if (this.fixSelectionOnEditableRoot(this.activeSelection)) {\n                return;\n            }\n        }\n        this.dispatchTo(\"selectionchange_handlers\", selectionData);\n    }\n\n    /**\n     * @param { Selection } [selection] The DOM selection\n     * @return { EditorSelection }\n     */\n    makeActiveSelection(selection) {\n        let range;\n        let activeSelection;\n        if (!selection || !selection.rangeCount) {\n            activeSelection = {\n                anchorNode: this.editable,\n                anchorOffset: 0,\n                focusNode: this.editable,\n                focusOffset: 0,\n                startContainer: this.editable,\n                startOffset: 0,\n                endContainer: this.editable,\n                endOffset: 0,\n                commonAncestorContainer: this.editable,\n                isCollapsed: true,\n                direction: DIRECTIONS.RIGHT,\n                textContent: () => \"\",\n                intersectsNode: () => false,\n            };\n        } else {\n            range = selection.getRangeAt(0);\n            let { anchorNode, anchorOffset, focusNode, focusOffset } = selection;\n            let direction =\n                anchorNode === range.startContainer ? DIRECTIONS.RIGHT : DIRECTIONS.LEFT;\n            if (anchorNode === focusNode && focusOffset < anchorOffset) {\n                direction = !direction;\n            }\n            if (\n                this.activeSelection &&\n                (isProtecting(anchorNode) ||\n                    (isProtected(anchorNode) && !isUnprotecting(anchorNode)))\n            ) {\n                // Keep the previous activeSelection in case of user interactions\n                // inside a protected zone.\n                return this.activeSelection;\n            }\n            [anchorNode, anchorOffset] = normalizeCursorPosition(\n                anchorNode,\n                anchorOffset,\n                direction ? \"left\" : \"right\"\n            );\n            [focusNode, focusOffset] = normalizeCursorPosition(\n                focusNode,\n                focusOffset,\n                direction ? \"right\" : \"left\"\n            );\n            const [startContainer, startOffset, endContainer, endOffset] =\n                direction === DIRECTIONS.RIGHT\n                    ? [anchorNode, anchorOffset, focusNode, focusOffset]\n                    : [focusNode, focusOffset, anchorNode, anchorOffset];\n            range = this.document.createRange();\n            range.setStart(startContainer, startOffset);\n            range.setEnd(endContainer, endOffset);\n\n            activeSelection = {\n                anchorNode,\n                anchorOffset,\n                focusNode,\n                focusOffset,\n                startContainer,\n                startOffset,\n                endContainer,\n                endOffset,\n                commonAncestorContainer: range.commonAncestorContainer,\n                isCollapsed: range.collapsed,\n                direction,\n                textContent: () => (range.collapsed ? \"\" : selection.toString()),\n                intersectsNode: (node) => range.intersectsNode(node),\n            };\n        }\n\n        Object.freeze(activeSelection);\n        return activeSelection;\n    }\n\n    /**\n     * @param { EditorSelection } selection\n     */\n    extractContent(selection) {\n        const range = new Range();\n        range.setStart(selection.startContainer, selection.startOffset);\n        range.setEnd(selection.endContainer, selection.endOffset);\n        this.setSelection({\n            anchorNode: selection.startContainer,\n            anchorOffset: selection.startOffset,\n        });\n        return range.extractContents();\n    }\n\n    /**\n     * @param { Node } anchorNode\n     * @param { number } anchorOffset\n     * @param { Node } focusNode\n     * @param { number } focusOffset\n     * @param { boolean } direction\n     *\n     * @return { EditorSelection }\n     */\n    createEditorSelection(anchorNode, anchorOffset, focusNode, focusOffset, direction) {\n        let startContainer, startOffset, endContainer, endOffset;\n        const range = new Range();\n        if (direction) {\n            [startContainer, startOffset] = [anchorNode, anchorOffset];\n            [endContainer, endOffset] = [focusNode, focusOffset];\n        } else {\n            [startContainer, startOffset] = [focusNode, focusOffset];\n            [endContainer, endOffset] = [anchorNode, anchorOffset];\n        }\n\n        range.setStart(startContainer, startOffset);\n        range.setEnd(endContainer, endOffset);\n        return Object.freeze({\n            ...this.activeSelection,\n            anchorNode,\n            anchorOffset,\n            focusNode,\n            focusOffset,\n            startContainer,\n            startOffset,\n            endContainer,\n            endOffset,\n            commonAncestorContainer: range.commonAncestorContainer,\n            cloneContents: () => range.cloneContents(),\n        });\n    }\n    /**\n     @return { EditorSelection }\n     */\n    getEditableSelection() {\n        return this.getSelectionData().editableSelection;\n    }\n\n    /**\n     * @return { SelectionData }\n     */\n    getSelectionData() {\n        const selection = this.document.getSelection();\n        const documentSelectionIsInEditable = selection && this.isSelectionInEditable(selection);\n        const documentSelection =\n            selection?.anchorNode && selection?.focusNode\n                ? Object.freeze({\n                      anchorNode: selection.anchorNode,\n                      anchorOffset: selection.anchorOffset,\n                      focusNode: selection.focusNode,\n                      focusOffset: selection.focusOffset,\n                      commonAncestorContainer: selection.rangeCount\n                          ? selection.getRangeAt(0).commonAncestorContainer\n                          : null,\n                  })\n                : null;\n        if (documentSelectionIsInEditable) {\n            this.activeSelection = this.makeActiveSelection(selection);\n        } else if (!this.activeSelection.anchorNode.isConnected) {\n            this.activeSelection = this.makeActiveSelection();\n        }\n        let { anchorNode, anchorOffset, focusNode, focusOffset, isCollapsed, direction } =\n            this.activeSelection;\n\n        const editableSelection = this.createEditorSelection(\n            anchorNode,\n            anchorOffset,\n            focusNode,\n            focusOffset,\n            direction\n        );\n\n        const selectionData = {\n            documentSelection: documentSelection,\n            editableSelection: editableSelection,\n            documentSelectionIsInEditable: documentSelectionIsInEditable,\n        };\n\n        Object.defineProperty(selectionData, \"deepEditableSelection\", {\n            get: function () {\n                // Transform the selection to return the depest possible node.\n                [anchorNode, anchorOffset] = getDeepestPosition(anchorNode, anchorOffset);\n                [focusNode, focusOffset] = isCollapsed\n                    ? [anchorNode, anchorOffset]\n                    : getDeepestPosition(focusNode, focusOffset);\n                return this.createEditorSelection(\n                    anchorNode,\n                    anchorOffset,\n                    focusNode,\n                    focusOffset,\n                    direction\n                );\n            }.bind(this),\n        });\n\n        Object.defineProperty(selectionData, \"documentSelectionIsProtecting\", {\n            get: function () {\n                return documentSelection?.anchorNode\n                    ? isProtecting(documentSelection.anchorNode)\n                    : false;\n            }.bind(this),\n        });\n        Object.defineProperty(selectionData, \"documentSelectionIsProtected\", {\n            get: function () {\n                return documentSelection?.anchorNode\n                    ? isProtected(documentSelection.anchorNode)\n                    : false;\n            }.bind(this),\n        });\n\n        return Object.freeze(selectionData);\n    }\n\n    /**\n     * Set the selection in the editor.\n     *\n     * @param { Object } selection\n     * @param { Node } selection.anchorNode\n     * @param { number } selection.anchorOffset\n     * @param { Node } [selection.focusNode=selection.anchorNode]\n     * @param { number } [selection.focusOffset=selection.anchorOffset]\n     * @param { Object } [options]\n     * @param { boolean } [options.normalize=true] Normalize deep the selection\n     * @return { EditorSelection }\n     */\n    setSelection(\n        { anchorNode, anchorOffset, focusNode = anchorNode, focusOffset = anchorOffset },\n        { normalize = true } = {}\n    ) {\n        if (!this.isSelectionInEditable({ anchorNode, focusNode })) {\n            throw new Error(\"Selection is not in editor\");\n        }\n        const isCollapsed = anchorNode === focusNode && anchorOffset === focusOffset;\n        [focusNode, focusOffset] = normalizeCursorPosition(focusNode, focusOffset, \"right\");\n        [anchorNode, anchorOffset] = isCollapsed\n            ? [focusNode, focusOffset]\n            : normalizeCursorPosition(anchorNode, anchorOffset, \"left\");\n        if (normalize) {\n            // normalize selection\n            [anchorNode, anchorOffset] = normalizeDeepCursorPosition(anchorNode, anchorOffset);\n            [focusNode, focusOffset] = isCollapsed\n                ? [anchorNode, anchorOffset]\n                : normalizeDeepCursorPosition(focusNode, focusOffset);\n        }\n\n        [anchorNode, anchorOffset] = normalizeFakeBR(anchorNode, anchorOffset);\n        [focusNode, focusOffset] = normalizeFakeBR(focusNode, focusOffset);\n        const selection = this.document.getSelection();\n        const documentSelectionIsInEditable = selection && this.isSelectionInEditable(selection);\n        if (selection) {\n            if (documentSelectionIsInEditable || selection.anchorNode === null) {\n                selection.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);\n                this.activeSelection = this.makeActiveSelection(selection, true);\n            } else {\n                let range = new Range();\n                range.setStart(anchorNode, anchorOffset);\n                range.setEnd(focusNode, focusOffset);\n                if (anchorNode !== focusNode || anchorOffset !== focusOffset) {\n                    // Check if the direction is correct\n                    if (range.collapsed) {\n                        range = new Range();\n                        range.setEnd(anchorNode, anchorOffset);\n                        range.setStart(focusNode, focusOffset);\n                    }\n                }\n\n                this.activeSelection = this.makeActiveSelection({\n                    anchorNode,\n                    anchorOffset,\n                    focusNode,\n                    focusOffset,\n                    getRangeAt: () => range,\n                    rangeCount: 1,\n                });\n            }\n        }\n\n        return this.activeSelection;\n    }\n\n    /**\n     * Set the cursor at the start of the given node.\n     * @param { Node } node\n     */\n    setCursorStart(node) {\n        return this.setSelection({ anchorNode: node, anchorOffset: 0 });\n    }\n\n    /**\n     * Set the cursor at the end of the given node.\n     * @param { Node } node\n     */\n    setCursorEnd(node) {\n        return this.setSelection({ anchorNode: node, anchorOffset: nodeSize(node) });\n    }\n\n    /**\n     * Stores the current selection and returns an object with methods to:\n     * - update the cursors (anchor and focus) node and offset after DOM\n     * manipulations that migh affect them. Such methods are chainable.\n     * - restore the updated selection.\n     * @returns {Cursors}\n     */\n    preserveSelection() {\n        const hadSelection =\n            this.document.getSelection() && this.document.getSelection().anchorNode !== null;\n        const selectionData = this.getSelectionData();\n        const selection = selectionData.editableSelection;\n        const anchor = { node: selection.anchorNode, offset: selection.anchorOffset };\n        const focus = { node: selection.focusNode, offset: selection.focusOffset };\n\n        return {\n            restore: () => {\n                if (!hadSelection) {\n                    return;\n                }\n                this.setSelection(\n                    {\n                        anchorNode: anchor.node,\n                        anchorOffset: anchor.offset,\n                        focusNode: focus.node,\n                        focusOffset: focus.offset,\n                    },\n                    { normalize: false }\n                );\n            },\n            update(callback) {\n                callback(anchor);\n                callback(focus);\n                return this;\n            },\n            remapNode(node, newNode) {\n                return this.update((cursor) => {\n                    if (cursor.node === node) {\n                        cursor.node = newNode;\n                    }\n                });\n            },\n            setOffset(node, newOffset) {\n                return this.update((cursor) => {\n                    if (cursor.node === node) {\n                        cursor.offset = newOffset;\n                    }\n                });\n            },\n            shiftOffset(node, shiftOffset) {\n                return this.update((cursor) => {\n                    if (cursor.node === node) {\n                        cursor.offset += shiftOffset;\n                    }\n                });\n            },\n        };\n    }\n\n    /**\n     * Returns an array containing all the nodes fully contained in the selection.\n     *\n     * @returns {Node[]}\n     */\n    getSelectedNodes() {\n        const selection = this.getSelectionData().editableSelection;\n        const range = new Range();\n        range.setStart(selection.startContainer, selection.startOffset);\n        range.setEnd(selection.endContainer, selection.endOffset);\n        const isNodeFullySelected = (node) =>\n            // Custom rules\n            this.getResource(\"fully_selected_node_predicates\").some((cb) => cb(node, selection)) ||\n            // Default rule\n            (range.isPointInRange(node, 0) && range.isPointInRange(node, nodeSize(node)));\n        return this.getTraversedNodes().filter(isNodeFullySelected);\n    }\n\n    /**\n     * Returns the nodes intersected by the current selection, up to the common\n     * ancestor container (inclusive).\n     *\n     * @returns {Node[]}\n     */\n    getTraversedNodes() {\n        const selection = this.getSelectionData().deepEditableSelection;\n        const { commonAncestorContainer: root } = selection;\n\n        let traversedNodes = [\n            root,\n            ...descendants(root).filter((node) => selection.intersectsNode(node)),\n        ];\n\n        const modifiers = [\n            // Remove the editable from the list\n            (nodes) => (nodes[0] === this.editable ? nodes.slice(1) : nodes),\n            // Filter out nodes that have no content selected\n            (nodes) => {\n                const edgeNodes = getUnselectedEdgeNodes(selection);\n                return nodes.filter((node) => !edgeNodes.has(node));\n            },\n            // Custom modifiers\n            ...this.getResource(\"traversed_nodes_processors\"),\n        ];\n\n        for (const modifier of modifiers) {\n            traversedNodes = modifier(traversedNodes);\n        }\n\n        return traversedNodes;\n    }\n\n    /**\n     * Returns a Set of traversed blocks within the given range.\n     *\n     * @returns {Set<HTMLElement>}\n     */\n    getTraversedBlocks() {\n        return new Set(this.getTraversedNodes().map(closestBlock).filter(Boolean));\n    }\n    resetActiveSelection() {\n        const selection = this.document.getSelection();\n        selection.setBaseAndExtent(\n            this.previousActiveSelection.anchorNode,\n            this.previousActiveSelection.anchorOffset,\n            this.previousActiveSelection.focusNode,\n            this.previousActiveSelection.focusOffset\n        );\n    }\n\n    // @todo @phoenix we should find a real use case and test it\n    // /**\n    //  * Set a deep selection that split the text and collapse it if only one ZWS is\n    //  * selected.\n    //  *\n    //  * @returns {boolean} true if the selection has only one ZWS.\n    //  */\n    // collapseIfZWS() {\n    //     const selection = this.getSelectionData().deepEditableSelection;\n    //     if (\n    //         selection.startContainer === selection.endContainer &&\n    //         selection.startContainer.nodeType === Node.TEXT_NODE &&\n    //         selection.startContainer.textContent === \"\\u200B\"\n    //     ) {\n    //         // We Collapse the selection and bypass deleteRange\n    //         // if the range content is only one ZWS.\n    //         this.setCursorStart(selection.startContainer);\n    //         return true;\n    //     }\n    //     return false;\n    // }\n\n    /**\n     * Places the cursor in a safe place (not the editable root).\n     * Inserts an empty paragraph if selection results from mouse click and\n     * there's no other way to insert text before/after a block.\n     *\n     * @param {Selection} selection - Collapsed selection at the editable root.\n     */\n    fixSelectionOnEditableRoot(selection) {\n        if (!selection.isCollapsed || selection.anchorNode !== this.editable) {\n            return false;\n        }\n\n        this.dispatchTo(\"fix_selection_on_editable_root_handlers\", selection);\n    }\n\n    /**\n     * This function adjusts a given selection to the current nodeSize of its\n     * anchorNode and focusNode, only if they are both present in the given\n     * editable. Apply and return: a valid given selection, a modified\n     * selection if some offset needed to be adjusted. Do nothing if the given\n     * selection anchor or focus nodes are not in this.editable.\n     *\n     * @param { Object } selection\n     * @param { Node } selection.anchorNode\n     * @param { number } selection.anchorOffset\n     * @param { Node } selection.focusNode\n     * @param { number } selection.focusOffset\n     * @returns { EditorSelection|null } selection, rectified selection or null\n     */\n    rectifySelection(selection) {\n        if (!this.isSelectionInEditable(selection)) {\n            return null;\n        }\n        const anchorNode = selection.anchorNode;\n        let anchorOffset = selection.anchorOffset;\n        const focusNode = selection.focusNode;\n        let focusOffset = selection.focusOffset;\n        const anchorSize = nodeSize(anchorNode);\n        const focusSize = nodeSize(focusNode);\n        if (anchorSize < anchorOffset) {\n            anchorOffset = anchorSize;\n        }\n        if (focusSize < focusOffset) {\n            focusOffset = focusSize;\n        }\n        const anchorTarget = childNodes(anchorNode).at(anchorOffset);\n        const focusTarget = childNodes(focusNode).at(focusOffset);\n        const protectionCheck = (node) =>\n            isProtecting(node) || (isProtected(node) && !isUnprotecting(node));\n        if (\n            focusTarget !== anchorTarget &&\n            focusTarget.previousSibling === anchorTarget &&\n            protectionCheck(anchorTarget)\n        ) {\n            return;\n        }\n        if (protectionCheck(anchorNode) || protectionCheck(focusNode)) {\n            // TODO @phoenix, TODO ABD: better handle setSelection on protected\n            // elements\n            return;\n        }\n        return this.setSelection({\n            anchorNode,\n            anchorOffset,\n            focusNode,\n            focusOffset,\n        });\n    }\n\n    /**\n     * @param {\"move\"|\"extend\"} alter\n     * @param {\"backward\"|\"forward\"} direction\n     * @param {\"character\"|\"word\"|\"line\"} granularity\n     * @returns {EditorSelection}\n     */\n    modifySelection(alter, direction, granularity) {\n        const selectionData = this.getSelectionData();\n        if (!selectionData.documentSelectionIsInEditable) {\n            return selectionData.editableSelection;\n        }\n        const selection = this.document.getSelection();\n        if (!selection) {\n            return selectionData.editableSelection;\n        }\n        selection.modify(alter, direction, granularity);\n        if (!this.isSelectionInEditable(selection)) {\n            // If selection was moved to outside the editable, restore it.\n            return this.setSelection(selectionData.editableSelection);\n        }\n        this.activeSelection = this.makeActiveSelection(selection);\n        return this.activeSelection;\n    }\n\n    /**\n     * Changes the selection before the browser's default behavior moves the\n     * cursor, in order to skip undesired characters (typically invisible\n     * characters).\n     */\n    onKeyDownArrows(ev) {\n        const selection = this.document.getSelection();\n        if (!selection || !this.isSelectionInEditable(selection)) {\n            return;\n        }\n\n        // Whether moving a collapsed cursor or extending a selection.\n        const mode = ev.shiftKey ? \"extend\" : \"move\";\n\n        if ([\"ArrowLeft\", \"ArrowRight\"].includes(ev.key)) {\n            // Direction of the movement (take rtl writing into account)\n            const screenDirection = ev.key === \"ArrowLeft\" ? \"left\" : \"right\";\n            const isRtl = closestElement(selection.focusNode, \"[dir]\")?.dir === \"rtl\";\n            const domDirection = (screenDirection === \"left\") ^ isRtl ? \"previous\" : \"next\";\n\n            // Whether the character next to the cursor should be skipped.\n            const shouldSkipCallbacks = this.getResource(\n                \"intangible_char_for_keyboard_navigation_predicates\"\n            );\n            let adjacentCharacter = getAdjacentCharacter(selection, domDirection, this.editable);\n            let shouldSkip = shouldSkipCallbacks.some((cb) => cb(ev, adjacentCharacter));\n\n            while (shouldSkip) {\n                const { focusNode: nodeBefore, focusOffset: offsetBefore } = selection;\n\n                selection.modify(mode, screenDirection, \"character\");\n\n                const hasSelectionChanged =\n                    nodeBefore !== selection.focusNode || offsetBefore !== selection.focusOffset;\n                const lastSkippedChar = adjacentCharacter;\n                adjacentCharacter = getAdjacentCharacter(selection, domDirection, this.editable);\n\n                shouldSkip =\n                    hasSelectionChanged &&\n                    shouldSkipCallbacks.some((cb) => cb(ev, adjacentCharacter, lastSkippedChar));\n            }\n        }\n\n        const { focusNode, focusOffset } = selection;\n        if (mode === \"extend\") {\n            // Since selection can't traverse contenteditable=\"false\" elements,\n            // we adjust the selection to the sibling of non editable element.\n            const selectingBackward = [\"ArrowLeft\", \"ArrowUp\"].includes(ev.key);\n            const currentBlock = closestBlock(focusNode);\n            const isAtBoundary = selectingBackward\n                ? firstLeaf(currentBlock) === focusNode && focusOffset === 0\n                : lastLeaf(currentBlock) === focusNode && focusOffset === nodeSize(focusNode);\n            const adjacentBlock = selectingBackward\n                ? currentBlock.previousElementSibling\n                : currentBlock.nextElementSibling;\n            const targetBlock = selectingBackward\n                ? adjacentBlock?.previousElementSibling\n                : adjacentBlock?.nextElementSibling;\n            if (!adjacentBlock?.isContentEditable && targetBlock && isAtBoundary) {\n                const leafNode = selectingBackward ? lastLeaf(targetBlock) : firstLeaf(targetBlock);\n                const offset = selectingBackward ? nodeSize(leafNode) : 0;\n                selection.extend(leafNode, offset);\n                ev.preventDefault();\n            }\n        }\n    }\n\n    isSelectionInEditable({ anchorNode, focusNode } = {}) {\n        return (\n            !!anchorNode &&\n            !!focusNode &&\n            this.editable.contains(anchorNode) &&\n            (focusNode === anchorNode || this.editable.contains(focusNode))\n        );\n    }\n\n    focusEditable() {\n        const { editableSelection, documentSelectionIsInEditable } = this.getSelectionData();\n        if (documentSelectionIsInEditable) {\n            return;\n        }\n        // Manualy focusing the editable is necessary to avoid some non-deterministic error in the HOOT unit tests.\n        this.editable.focus();\n        const { anchorNode, anchorOffset, focusNode, focusOffset } = editableSelection;\n        const selection = this.document.getSelection();\n        if (selection) {\n            selection.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);\n        }\n    }\n}\n", "import { Plugin } from \"../plugin\";\n\n/**\n * @typedef {Object} Shortcut\n * @property {string} hotkey\n * @property {string} commandId\n * @property {Object} [commandParams]\n *\n * Example:\n *\n *     resources = {\n *         user_commands: [\n *             { id: \"myCommands\", run: myCommandFunction },\n *         ],\n *         shortcuts: [\n *             { hotkey: \"control+shift+q\", commandId: \"myCommands\" },\n *         ],\n *     }\n */\n\nexport class ShortCutPlugin extends Plugin {\n    static id = \"shortcut\";\n    static dependencies = [\"userCommand\"];\n\n    setup() {\n        const hotkeyService = this.services.hotkey;\n        if (!hotkeyService) {\n            throw new Error(\"ShorcutPlugin needs hotkey service to properly work\");\n        }\n        if (document !== this.document) {\n            hotkeyService.registerIframe({ contentWindow: this.document.defaultView });\n        }\n        for (const shortcut of this.getResource(\"shortcuts\")) {\n            const command = this.dependencies.userCommand.getCommand(shortcut.commandId);\n            this.addShortcut(shortcut.hotkey, () => {\n                command.run(shortcut.commandParams);\n            });\n        }\n    }\n\n    addShortcut(hotkey, action) {\n        this.services.hotkey.add(hotkey, action, {\n            area: () => this.editable,\n            bypassEditableProtection: true,\n            allowRepeat: true,\n        });\n    }\n}\n", "import { Plugin } from \"../plugin\";\nimport { isBlock } from \"../utils/blocks\";\nimport { fillEmpty, splitTextNode } from \"../utils/dom\";\nimport {\n    isContentEditable,\n    isContentEditableAncestor,\n    isTextNode,\n    isVisible,\n} from \"../utils/dom_info\";\nimport { prepareUpdate } from \"../utils/dom_state\";\nimport { childNodes, closestElement, firstLeaf, lastLeaf } from \"../utils/dom_traversal\";\nimport { DIRECTIONS, childNodeIndex, nodeSize } from \"../utils/position\";\nimport { isProtected, isProtecting } from \"@html_editor/utils/dom_info\";\n\n/**\n * @typedef { Object } SplitShared\n * @property { SplitPlugin['isUnsplittable'] } isUnsplittable\n * @property { SplitPlugin['splitAroundUntil'] } splitAroundUntil\n * @property { SplitPlugin['splitBlock'] } splitBlock\n * @property { SplitPlugin['splitBlockNode'] } splitBlockNode\n * @property { SplitPlugin['splitElement'] } splitElement\n * @property { SplitPlugin['splitElementBlock'] } splitElementBlock\n * @property { SplitPlugin['splitSelection'] } splitSelection\n */\n\nexport class SplitPlugin extends Plugin {\n    static dependencies = [\"baseContainer\", \"selection\", \"history\", \"input\", \"delete\", \"lineBreak\"];\n    static id = \"split\";\n    static shared = [\n        \"splitBlock\",\n        \"splitBlockNode\",\n        \"splitElementBlock\",\n        \"splitElement\",\n        \"splitAroundUntil\",\n        \"splitSelection\",\n        \"isUnsplittable\",\n    ];\n    resources = {\n        beforeinput_handlers: this.onBeforeInput.bind(this),\n\n        unsplittable_node_predicates: [\n            // An unremovable element is also unmergeable (as merging two\n            // elements results in removing one of them).\n            // An unmergeable element is unsplittable and vice-versa (as\n            // split and merge are reverse operations from one another).\n            // Therefore, unremovable nodes are also unsplittable.\n            (node) =>\n                this.getResource(\"unremovable_node_predicates\").some((predicate) =>\n                    predicate(node)\n                ),\n            // \"Unbreakable\" is a legacy term that means unsplittable and\n            // unmergeable.\n            (node) => node.classList?.contains(\"oe_unbreakable\"),\n            (node) => {\n                const isExplicitlyNotContentEditable = (node) => {\n                    // In the `contenteditable` attribute consideration,\n                    // disconnected nodes can be unsplittable only if they are\n                    // explicitly set under a contenteditable=\"false\" element.\n                    return (\n                        !isContentEditable(node) &&\n                        (node.isConnected || closestElement(node, \"[contenteditable]\"))\n                    );\n                };\n                return (\n                    isExplicitlyNotContentEditable(node) ||\n                    // If node sets contenteditable='true' and is inside a non-editable\n                    // context, it has to be unsplittable since splitting it would modify\n                    // the non-editable parent content.\n                    (node.parentElement &&\n                        isContentEditableAncestor(node) &&\n                        isExplicitlyNotContentEditable(node.parentElement))\n                );\n            },\n            (node) => node.nodeName === \"SECTION\",\n        ],\n    };\n\n    // --------------------------------------------------------------------------\n    // commands\n    // --------------------------------------------------------------------------\n    splitBlock() {\n        this.dispatchTo(\"before_split_block_handlers\");\n        let selection = this.dependencies.selection.getEditableSelection();\n        if (!selection.isCollapsed) {\n            // @todo @phoenix collapseIfZWS is not tested\n            // this.shared.collapseIfZWS();\n            this.dependencies.delete.deleteSelection();\n            selection = this.dependencies.selection.getEditableSelection();\n        }\n\n        return this.splitBlockNode({\n            targetNode: selection.anchorNode,\n            targetOffset: selection.anchorOffset,\n        });\n    }\n\n    /**\n     * @param {Object} param0\n     * @param {Node} param0.targetNode\n     * @param {number} param0.targetOffset\n     * @returns {[HTMLElement|undefined, HTMLElement|undefined]}\n     */\n    splitBlockNode({ targetNode, targetOffset }) {\n        if (targetNode.nodeType === Node.TEXT_NODE) {\n            targetOffset = splitTextNode(targetNode, targetOffset);\n            targetNode = targetNode.parentElement;\n        }\n        const blockToSplit = closestElement(targetNode, isBlock);\n        const params = { targetNode, targetOffset, blockToSplit };\n\n        if (this.delegateTo(\"split_element_block_overrides\", params)) {\n            return [undefined, undefined];\n        }\n\n        return this.splitElementBlock(params);\n    }\n    /**\n     * @param {Object} param0\n     * @param {HTMLElement} param0.targetNode\n     * @param {number} param0.targetOffset\n     * @param {HTMLElement} param0.blockToSplit\n     * @returns {[HTMLElement|undefined, HTMLElement|undefined]}\n     */\n    splitElementBlock({ targetNode, targetOffset, blockToSplit }) {\n        // If the block is unsplittable, insert a line break instead.\n        if (this.isUnsplittable(blockToSplit)) {\n            // @todo: t-if, t-else etc are not blocks, but they are\n            // unsplittable.  The check must be done from the targetNode up to\n            // the block for unsplittables. There are apparently no tests for\n            // this.\n            this.dependencies.lineBreak.insertLineBreakElement({ targetNode, targetOffset });\n            return [undefined, undefined];\n        }\n        const restore = prepareUpdate(targetNode, targetOffset);\n\n        const [beforeElement, afterElement] = this.splitElementUntil(\n            targetNode,\n            targetOffset,\n            blockToSplit.parentElement\n        );\n        restore();\n        const removeEmptyAndFill = (node) => {\n            if (isProtecting(node) || isProtected(node)) {\n                // TODO ABD: add test\n                return;\n            } else if (!isBlock(node) && !isVisible(node)) {\n                const parent = node.parentElement;\n                node.remove();\n                removeEmptyAndFill(parent);\n            } else {\n                fillEmpty(node);\n            }\n        };\n        removeEmptyAndFill(lastLeaf(beforeElement));\n        removeEmptyAndFill(firstLeaf(afterElement));\n\n        this.dependencies.selection.setCursorStart(afterElement);\n\n        return [beforeElement, afterElement];\n    }\n\n    /**\n     * @param {Node} node\n     * @returns {boolean}\n     */\n    isUnsplittable(node) {\n        return this.getResource(\"unsplittable_node_predicates\").some((p) => p(node));\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 {HTMLElement} element\n     * @param {number} offset\n     * @returns {[HTMLElement, HTMLElement]}\n     */\n    splitElement(element, offset) {\n        this.dispatchTo(\"clean_handlers\", element);\n        // const before = /** @type {HTMLElement} **/ (element.cloneNode());\n        /** @type {HTMLElement} **/\n        const before = element.cloneNode();\n        const after = /** @type {HTMLElement} **/ (element.cloneNode());\n        element.before(before);\n        element.after(after);\n        let index = 0;\n        for (const child of childNodes(element)) {\n            index < offset ? before.appendChild(child) : after.appendChild(child);\n            index++;\n        }\n        element.remove();\n        return [before, after];\n    }\n\n    /**\n     * Split the given element at the given offset, until the given limit ancestor.\n     * The element will be removed in the process so caution is advised in dealing\n     * with its reference. Returns a tuple containing the new elements on both sides\n     * of the split.\n     *\n     * @param {HTMLElement} element\n     * @param {number} offset\n     * @param {HTMLElement} limitAncestor\n     * @returns {[HTMLElement, HTMLElement]}\n     */\n    splitElementUntil(element, offset, limitAncestor) {\n        if (element === limitAncestor) {\n            return [element, element];\n        }\n        let [before, after] = this.splitElement(element, offset);\n        if (after.parentElement !== limitAncestor) {\n            const afterIndex = childNodeIndex(after);\n            [before, after] = this.splitElementUntil(\n                after.parentElement,\n                afterIndex,\n                limitAncestor\n            );\n        }\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     * @param {Node[] | Node} elements\n     * @param {HTMLElement} limitAncestor\n     * @returns { Node }\n     */\n    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 this.splitAroundUntil(elements[0].parentElement, limitAncestor);\n        }\n        // Split up ancestors up to font\n        while (after && after.parentElement !== limitAncestor) {\n            afterSplit = this.splitElement(after.parentElement, childNodeIndex(after))[0];\n            after = afterSplit.nextSibling;\n        }\n        if (after) {\n            afterSplit = this.splitElement(limitAncestor, childNodeIndex(after))[0];\n            limitAncestor = afterSplit;\n        }\n        while (before && before.parentElement !== limitAncestor) {\n            beforeSplit = this.splitElement(before.parentElement, childNodeIndex(before) + 1)[1];\n            before = beforeSplit.previousSibling;\n        }\n        if (before) {\n            beforeSplit = this.splitElement(limitAncestor, childNodeIndex(before) + 1)[1];\n        }\n        return beforeSplit || afterSplit || limitAncestor;\n    }\n\n    splitSelection() {\n        let { startContainer, startOffset, endContainer, endOffset, direction } =\n            this.dependencies.selection.getEditableSelection();\n        const isInSingleContainer = startContainer === endContainer;\n        if (isTextNode(endContainer) && endOffset > 0 && endOffset < nodeSize(endContainer)) {\n            const endParent = endContainer.parentNode;\n            const splitOffset = splitTextNode(endContainer, endOffset);\n            endContainer = endParent.childNodes[splitOffset - 1] || endParent.firstChild;\n            if (isInSingleContainer) {\n                startContainer = endContainer;\n            }\n            endOffset = endContainer.textContent.length;\n        }\n        if (\n            isTextNode(startContainer) &&\n            startOffset > 0 &&\n            startOffset < nodeSize(startContainer)\n        ) {\n            splitTextNode(startContainer, startOffset);\n            startOffset = 0;\n            if (isInSingleContainer) {\n                endOffset = startContainer.textContent.length;\n            }\n        }\n\n        const selection =\n            direction === DIRECTIONS.RIGHT\n                ? {\n                      anchorNode: startContainer,\n                      anchorOffset: startOffset,\n                      focusNode: endContainer,\n                      focusOffset: endOffset,\n                  }\n                : {\n                      anchorNode: endContainer,\n                      anchorOffset: endOffset,\n                      focusNode: startContainer,\n                      focusOffset: startOffset,\n                  };\n        return this.dependencies.selection.setSelection(selection, { normalize: false });\n    }\n\n    onBeforeInput(e) {\n        if (e.inputType === \"insertParagraph\") {\n            e.preventDefault();\n            this.splitBlock();\n            this.dependencies.history.addStep();\n        }\n    }\n}\n", "import { Plugin } from \"../plugin\";\n\n/**\n * @typedef { import(\"./selection_plugin\").EditorSelection } EditorSelection\n */\n\n/**\n * @typedef { Object } UserCommand\n * @property { string } id\n * @property { Function } run\n * @property { String } [title]\n * @property { String } [description]\n * @property { string } [icon]\n * @property { (selection: EditorSelection) => boolean  } [isAvailable]\n */\n\n/**\n * @typedef { Object } UserCommandShared\n * @property { UserCommandPlugin['getCommand'] } getCommand\n */\n\nexport class UserCommandPlugin extends Plugin {\n    static id = \"userCommand\";\n    static shared = [\"getCommand\"];\n\n    setup() {\n        this.commands = {};\n        for (const command of this.getResource(\"user_commands\")) {\n            if (command.id in this.commands) {\n                throw new Error(`Duplicate user command id: ${command.id}`);\n            }\n            this.commands[command.id] = command;\n        }\n        Object.freeze(this.commands);\n    }\n\n    /**\n     * @param {string} commandId\n     * @returns {UserCommand}\n     * @throws {Error} if the command ID is unknown.\n     */\n    getCommand(commandId) {\n        const command = this.commands[commandId];\n        if (!command) {\n            throw new Error(`Unknown user command id: ${commandId}`);\n        }\n        return command;\n    }\n}\n", "import { MAIN_PLUGINS } from \"./plugin_sets\";\nimport { createBaseContainer } from \"./utils/base_container\";\nimport { fillShrunkPhrasingParent, removeClass } from \"./utils/dom\";\nimport { isEmpty } from \"./utils/dom_info\";\nimport { resourceSequenceSymbol, withSequence } from \"./utils/resource\";\nimport { fixInvalidHTML, initElementForEdition } from \"./utils/sanitize\";\n\n/**\n * @typedef { import(\"./plugin_sets\").SharedMethods } SharedMethods\n * @typedef {typeof import(\"./plugin\").Plugin} PluginConstructor\n **/\n\n/**\n * @typedef { Object } CollaborationConfig\n * @property { string } collaboration.peerId\n * @property { Object } collaboration.busService\n * @property { Object } collaboration.collaborationChannel\n * @property { String } collaboration.collaborationChannel.collaborationModelName\n * @property { String } collaboration.collaborationChannel.collaborationFieldName\n * @property { Number } collaboration.collaborationChannel.collaborationResId\n * @property { 'start' | 'focus' } [collaboration.collaborativeTrigger]\n\n * @typedef { Object } EditorConfig\n * @property { string } [content]\n * @property { boolean } [allowInlineAtRoot]\n * @property { string } [baseContainer]\n * @property { PluginConstructor[] } [Plugins]\n * @property { boolean } [disableFloatingToolbar]\n * @property { string[] } [classList]\n * @property { Object } [localOverlayContainers]\n * @property { Object } [embeddedComponentInfo]\n * @property { Object } [resources]\n * @property { string } [direction=\"ltr\"]\n * @property { Function } [onChange]\n * @property { Function } [onEditorReady]\n * @property { boolean } [dropImageAsAttachment]\n * @property { CollaborationConfig } [collaboration]\n * @property { Function } getRecordInfo\n */\n\nfunction sortPlugins(plugins) {\n    const initialPlugins = new Set(plugins);\n    const inResult = new Set();\n    // need to sort them\n    const result = [];\n    let P;\n\n    function findPlugin() {\n        for (const P of initialPlugins) {\n            if (P.dependencies.every((dep) => inResult.has(dep))) {\n                initialPlugins.delete(P);\n                return P;\n            }\n        }\n    }\n    while ((P = findPlugin())) {\n        inResult.add(P.id);\n        result.push(P);\n    }\n    if (initialPlugins.size) {\n        const messages = [];\n        for (const P of initialPlugins) {\n            messages.push(\n                `\"${P.id}\" is missing (${P.dependencies\n                    .filter((d) => !inResult.has(d))\n                    .join(\", \")})`\n            );\n        }\n        throw new Error(`Missing dependencies:  ${messages.join(\", \")}`);\n    }\n    return result;\n}\n\nexport class Editor {\n    /**\n     * @param { EditorConfig } config\n     */\n    constructor(config, services) {\n        this.isDestroyed = false;\n        this.config = config;\n        this.services = services;\n        this.resources = null;\n        this.plugins = [];\n        /** @type { HTMLElement } **/\n        this.editable = null;\n        /** @type { Document } **/\n        this.document = null;\n        /** @ts-ignore  @type { SharedMethods } **/\n        this.shared = {};\n    }\n\n    attachTo(editable) {\n        if (this.isDestroyed || this.editable) {\n            throw new Error(\"Cannot re-attach an editor\");\n        }\n        this.editable = editable;\n        this.document = editable.ownerDocument;\n        if (this.config.content) {\n            editable.innerHTML = fixInvalidHTML(this.config.content);\n            if (isEmpty(editable)) {\n                const baseContainer = createBaseContainer(this.config.baseContainer, this.document);\n                fillShrunkPhrasingParent(baseContainer);\n                editable.replaceChildren(baseContainer);\n            }\n        }\n        this.preparePlugins();\n        editable.setAttribute(\"contenteditable\", true);\n        initElementForEdition(editable, { allowInlineAtRoot: !!this.config.allowInlineAtRoot });\n        editable.classList.add(\"odoo-editor-editable\");\n        if (this.config.classList) {\n            editable.classList.add(...this.config.classList);\n        }\n        if (this.config.height) {\n            editable.style.height = this.config.height;\n        }\n        this.startPlugins();\n        this.config.onEditorReady?.();\n    }\n\n    preparePlugins() {\n        const Plugins = sortPlugins(this.config.Plugins || MAIN_PLUGINS);\n        const plugins = new Map();\n        for (const P of Plugins) {\n            if (P.id === \"\") {\n                throw new Error(`Missing plugin id (class ${P.name})`);\n            }\n            if (plugins.has(P.id)) {\n                throw new Error(`Duplicate plugin id: ${P.id}`);\n            }\n            const imports = {};\n            for (const dep of P.dependencies) {\n                if (plugins.has(dep)) {\n                    imports[dep] = {};\n                    for (const h of plugins.get(dep).shared) {\n                        imports[dep][h] = this.shared[dep][h];\n                    }\n                } else {\n                    throw new Error(`Missing dependency for plugin ${P.id}: ${dep}`);\n                }\n            }\n            plugins.set(P.id, P);\n            const plugin = new P(this.document, this.editable, imports, this.config, this.services);\n            this.plugins.push(plugin);\n            const exports = {};\n            for (const h of P.shared) {\n                if (!(h in plugin)) {\n                    throw new Error(`Missing helper implementation: ${h} in plugin ${P.id}`);\n                }\n                exports[h] = plugin[h].bind(plugin);\n            }\n            this.shared[P.id] = exports;\n        }\n        const resources = this.createResources();\n        for (const plugin of this.plugins) {\n            plugin._resources = resources;\n        }\n        this.resources = resources;\n    }\n\n    startPlugins() {\n        for (const plugin of this.plugins) {\n            plugin.setup();\n        }\n        this.resources[\"normalize_handlers\"].forEach((cb) => cb(this.editable));\n        this.resources[\"start_edition_handlers\"].forEach((cb) => cb());\n    }\n\n    createResources() {\n        const resources = {};\n\n        function registerResources(obj) {\n            for (const key in obj) {\n                if (!(key in resources)) {\n                    resources[key] = [];\n                }\n                resources[key].push(obj[key]);\n            }\n        }\n        if (this.config.resources) {\n            registerResources(this.config.resources);\n        }\n        for (const plugin of this.plugins) {\n            if (plugin.resources) {\n                registerResources(plugin.resources);\n            }\n        }\n\n        for (const key in resources) {\n            const resource = resources[key]\n                .flat()\n                .map((r) => {\n                    const isObjectWithSequence =\n                        typeof r === \"object\" && r !== null && resourceSequenceSymbol in r;\n                    return isObjectWithSequence ? r : withSequence(10, r);\n                })\n                .sort((a, b) => a[resourceSequenceSymbol] - b[resourceSequenceSymbol])\n                .map((r) => r.object);\n\n            resources[key] = resource;\n            Object.freeze(resources[key]);\n        }\n\n        return Object.freeze(resources);\n    }\n\n    getContent() {\n        return this.getElContent().innerHTML;\n    }\n\n    getElContent() {\n        const el = this.editable.cloneNode(true);\n        this.resources[\"clean_for_save_handlers\"].forEach((cb) => cb({ root: el }));\n        return el;\n    }\n\n    destroy(willBeRemoved) {\n        if (this.editable) {\n            let plugin;\n            while ((plugin = this.plugins.pop())) {\n                plugin.destroy();\n            }\n            this.shared = {};\n            if (!willBeRemoved) {\n                // we only remove class/attributes when necessary. If we know that the editable\n                // element will be removed, no need to make changes that may require the browser\n                // to recompute the layout\n                this.editable.removeAttribute(\"contenteditable\");\n                removeClass(this.editable, \"odoo-editor-editable\");\n            }\n            this.editable = null;\n        }\n        this.isDestroyed = true;\n    }\n}\n", "import { HtmlUpgradeManager } from \"@html_editor/html_migrations/html_upgrade_manager\";\nimport { stripVersion } from \"@html_editor/html_migrations/html_migrations_utils\";\nimport { stripHistoryIds } from \"@html_editor/others/collaboration/collaboration_odoo_plugin\";\nimport {\n    COLLABORATION_PLUGINS,\n    DYNAMIC_PLACEHOLDER_PLUGINS,\n    EMBEDDED_COMPONENT_PLUGINS,\n    MAIN_PLUGINS,\n} from \"@html_editor/plugin_sets\";\nimport {\n    MAIN_EMBEDDINGS,\n    READONLY_MAIN_EMBEDDINGS,\n} from \"@html_editor/others/embedded_components/embedding_sets\";\nimport { normalizeHTML } from \"@html_editor/utils/html\";\nimport { Wysiwyg } from \"@html_editor/wysiwyg\";\nimport { Component, markup, status, useRef, useState } from \"@odoo/owl\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { Mutex } from \"@web/core/utils/concurrency\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { TranslationButton } from \"@web/views/fields/translation_button\";\nimport { HtmlViewer } from \"./html_viewer\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { fixInvalidHTML, instanceofMarkup } from \"@html_editor/utils/sanitize\";\n\n/**\n * Check whether the current value contains nodes that would break\n * on insertion inside an existing body.\n *\n * @returns {boolean} true if 'this.props.value' contains a node\n * that can only exist once per document.\n */\nfunction computeContainsComplexHTML(value) {\n    const domParser = new DOMParser();\n    if (!value) {\n        return false;\n    }\n    const parsedOriginal = domParser.parseFromString(value, \"text/html\");\n    return !!parsedOriginal.head.innerHTML.trim();\n}\n\nexport class HtmlField extends Component {\n    static template = \"html_editor.HtmlField\";\n    static props = {\n        ...standardFieldProps,\n        isCollaborative: { type: Boolean, optional: true },\n        collaborativeTrigger: { type: String, optional: true },\n        dynamicPlaceholder: { type: Boolean, optional: true, default: false },\n        dynamicPlaceholderModelReferenceField: { type: String, optional: true },\n        cssReadonlyAssetId: { type: String, optional: true },\n        sandboxedPreview: { type: Boolean, optional: true },\n        codeview: { type: Boolean, optional: true },\n        editorConfig: { type: Object, optional: true },\n        embeddedComponents: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        dynamicPlaceholder: false,\n    };\n    static components = {\n        Wysiwyg,\n        HtmlViewer,\n        TranslationButton,\n    };\n\n    setup() {\n        this.htmlUpgradeManager = new HtmlUpgradeManager();\n        this.mutex = new Mutex();\n\n        this.codeViewRef = useRef(\"codeView\");\n\n        const { model } = this.props.record;\n        useBus(model.bus, \"WILL_SAVE_URGENTLY\", () => this.commitChanges({ urgent: true }));\n        useBus(model.bus, \"NEED_LOCAL_CHANGES\", ({ detail }) =>\n            detail.proms.push(this.commitChanges())\n        );\n        this.busService = this.env.services.bus_service;\n        this.ormService = useService(\"orm\");\n\n        this.isDirty = false;\n        this.state = useState({\n            key: 0,\n            showCodeView: false,\n            containsComplexHTML: computeContainsComplexHTML(\n                this.props.record.data[this.props.name]\n            ),\n        });\n\n        useRecordObserver((record) => {\n            // Reset Wysiwyg when we discard or onchange value\n            const newValue = fixInvalidHTML(record.data[this.props.name]);\n            if (!this.isDirty) {\n                const value = normalizeHTML(newValue, this.clearElementToCompare.bind(this));\n                if (this.lastValue !== value) {\n                    this.state.key++;\n                    this.state.containsComplexHTML = computeContainsComplexHTML(newValue);\n                    this.lastValue = value;\n                }\n            }\n        });\n        useRecordObserver((record) => {\n            const value = record.data[this.props.dynamicPlaceholderModelReferenceField || \"model\"];\n            // update Dynamic Placeholder reference model\n            if (this.props.dynamicPlaceholder && this.editor) {\n                this.editor.shared.dynamicPlaceholder?.updateDphDefaultModel(value);\n            }\n        });\n    }\n\n    get value() {\n        const value = this.props.record.data[this.props.name];\n        const newVal = this.htmlUpgradeManager.processForUpgrade(fixInvalidHTML(value), {\n            containsComplexHTML: this.state.containsComplexHTML,\n            env: this.env,\n        });\n        if (instanceofMarkup(value)) {\n            return markup(newVal);\n        }\n        return newVal;\n    }\n\n    get displayReadonly() {\n        return this.props.readonly || (this.sandboxedPreview && !this.state.showCodeView);\n    }\n\n    get wysiwygKey() {\n        return `${this.props.record.resId}_${this.state.key}`;\n    }\n\n    get sandboxedPreview() {\n        // @todo @phoenix maybe remove containsComplexHTML and alway use sandboxedPreview options\n        return this.props.sandboxedPreview || this.state.containsComplexHTML;\n    }\n\n    get isTranslatable() {\n        return this.props.record.fields[this.props.name].translate;\n    }\n\n    clearElementToCompare(element) {\n        if (this.props.isCollaborative) {\n            stripHistoryIds(element);\n        }\n        stripVersion(element);\n    }\n\n    async updateValue(value) {\n        this.lastValue = normalizeHTML(value, this.clearElementToCompare.bind(this));\n        this.isDirty = false;\n        await this.props.record.update({ [this.props.name]: value }).catch(() => {\n            this.isDirty = true;\n        });\n        this.props.record.model.bus.trigger(\"FIELD_IS_DIRTY\", this.isDirty);\n    }\n\n    async getEditorContent() {\n        await this.editor.shared.media?.savePendingImages();\n        return this.editor.getElContent();\n    }\n\n    async _commitChanges({ urgent }) {\n        if (status(this) === \"destroyed\") {\n            return;\n        }\n        if (this.isDirty) {\n            if (this.state.showCodeView) {\n                await this.updateValue(this.codeViewRef.el.value);\n                return;\n            }\n            if (urgent) {\n                await this.updateValue(this.editor.getContent());\n            }\n            const el = await this.getEditorContent();\n            const content = el.innerHTML;\n            this.clearElementToCompare(el);\n            const comparisonValue = el.innerHTML;\n            if (!urgent || (urgent && this.lastValue !== comparisonValue)) {\n                await this.updateValue(content);\n            }\n        }\n    }\n\n    async commitChanges({ urgent } = {}) {\n        if (urgent) {\n            this._commitChanges({ urgent });\n        } else {\n            return this.mutex.exec(() => this._commitChanges({ urgent }));\n        }\n    }\n\n    onEditorLoad(editor) {\n        this.editor = editor;\n    }\n\n    onChange() {\n        this.isDirty = true;\n        this.props.record.model.bus.trigger(\"FIELD_IS_DIRTY\", true);\n    }\n\n    onBlur() {\n        return this.commitChanges();\n    }\n\n    async toggleCodeView() {\n        await this.commitChanges();\n        this.state.showCodeView = !this.state.showCodeView;\n        if (!this.state.showCodeView && this.editor) {\n            this.editor.editable.innerHTML = this.value;\n            this.editor.shared.history.addStep();\n        }\n    }\n\n    getConfig() {\n        const config = {\n            content: this.value,\n            Plugins: [\n                ...MAIN_PLUGINS,\n                ...(this.props.isCollaborative ? COLLABORATION_PLUGINS : []),\n                ...(this.props.dynamicPlaceholder ? DYNAMIC_PLACEHOLDER_PLUGINS : []),\n                ...(this.props.embeddedComponents ? EMBEDDED_COMPONENT_PLUGINS : []),\n            ],\n            classList: this.classList,\n            onChange: this.onChange.bind(this),\n            collaboration: this.props.isCollaborative && {\n                busService: this.busService,\n                ormService: this.ormService,\n                collaborativeTrigger: this.props.collaborativeTrigger,\n                collaborationChannel: {\n                    collaborationModelName: this.props.record.resModel,\n                    collaborationFieldName: this.props.name,\n                    collaborationResId: parseInt(this.props.record.resId),\n                },\n                peerId: this.generateId(),\n            },\n            dropImageAsAttachment: true, // @todo @phoenix always true ?\n            dynamicPlaceholder: this.dynamicPlaceholder,\n            dynamicPlaceholderResModel:\n                this.props.record.data[this.props.dynamicPlaceholderModelReferenceField || \"model\"],\n            direction: localization.direction || \"ltr\",\n            getRecordInfo: () => {\n                const { resModel, resId } = this.props.record;\n                return { resModel, resId };\n            },\n            resources: {},\n            ...this.props.editorConfig,\n        };\n\n        if (!(\"baseContainer\" in config)) {\n            config.baseContainer = \"DIV\";\n        }\n\n        if (this.props.embeddedComponents) {\n            // TODO @engagement: fill this array with default/base components\n            config.resources.embedded_components = [...MAIN_EMBEDDINGS];\n        }\n\n        const { sanitize_tags, sanitize } = this.props.record.fields[this.props.name];\n        if (\n            !(\"disableVideo\" in config) &&\n            (sanitize_tags || (sanitize_tags === undefined && sanitize))\n        ) {\n            config.disableVideo = true; // Tag-sanitized fields remove videos.\n        }\n        if (this.props.codeview) {\n            config.resources = {\n                user_commands: [\n                    {\n                        id: \"codeview\",\n                        title: _t(\"Code view\"),\n                        icon: \"fa-code\",\n                        run: this.toggleCodeView.bind(this),\n                    },\n                ],\n                toolbar_groups: withSequence(100, {\n                    id: \"codeview\",\n                }),\n                toolbar_items: {\n                    id: \"codeview\",\n                    groupId: \"codeview\",\n                    commandId: \"codeview\",\n                },\n            };\n        }\n        return config;\n    }\n\n    getReadonlyConfig() {\n        const config = {\n            value: this.value,\n            cssAssetId: this.props.cssReadonlyAssetId,\n            hasFullHtml: this.sandboxedPreview,\n            isFixedValue: true,\n        };\n        if (this.props.embeddedComponents) {\n            config.embeddedComponents = [...READONLY_MAIN_EMBEDDINGS];\n        }\n        return config;\n    }\n\n    generateId() {\n        // No need for secure random number.\n        return Math.floor(Math.random() * Math.pow(2, 52)).toString();\n    }\n}\n\nexport const htmlField = {\n    component: HtmlField,\n    displayName: _t(\"Html\"),\n    supportedTypes: [\"html\"],\n    extractProps({ attrs, options }, dynamicInfo) {\n        const editorConfig = {\n            mediaModalParams: {\n                useMediaLibrary: true,\n            },\n        };\n        if (attrs.placeholder) {\n            editorConfig.placeholder = attrs.placeholder;\n        }\n        if (options.height) {\n            editorConfig.height = `${options.height}px`;\n        }\n        if (\"disableImage\" in options) {\n            editorConfig.disableImage = Boolean(options.disableImage);\n        }\n        if (\"disableVideo\" in options) {\n            editorConfig.disableVideo = Boolean(options.disableVideo);\n        }\n        if (\"disableFile\" in options) {\n            editorConfig.disableFile = Boolean(options.disableFile);\n        }\n        if (\"baseContainer\" in options) {\n            editorConfig.baseContainer = options.baseContainer;\n        }\n        return {\n            editorConfig,\n            isCollaborative: options.collaborative,\n            collaborativeTrigger: options.collaborative_trigger,\n            dynamicPlaceholder: options.dynamic_placeholder,\n            dynamicPlaceholderModelReferenceField:\n                options.dynamic_placeholder_model_reference_field,\n            embeddedComponents:\n                \"embedded_components\" in options ? options.embedded_components : true,\n            sandboxedPreview: Boolean(options.sandboxedPreview),\n            cssReadonlyAssetId: options.cssReadonly,\n            codeview: Boolean(odoo.debug && options.codeview),\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"html\", htmlField, { force: true });\n", "import {\n    Component,\n    markup,\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 { fixInvalidHTML, instanceofMarkup } from \"@html_editor/utils/sanitize\";\nimport { HtmlUpgradeManager } from \"@html_editor/html_migrations/html_upgrade_manager\";\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.htmlUpgradeManager = new HtmlUpgradeManager();\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     * Typically, if the value comes from the html_field, it is already fixed\n     * (invalid and obsolete elements were replaced). If used as a standalone,\n     * the HtmlViewer has to handle invalid nodes and html upgrades.\n     *\n     * @param { string | Markup } value\n     * @returns { string | Markup }\n     */\n    formatValue(value) {\n        if (this.props.config.isFixedValue) {\n            return value;\n        }\n        const newVal = this.htmlUpgradeManager.processForUpgrade(fixInvalidHTML(value), {\n            env: this.env,\n        });\n        if (instanceofMarkup(value)) {\n            return markup(newVal);\n        }\n        return newVal;\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({ root, host }) {\n        const { getEditableDescendants } = this.getEmbedding(host);\n        const editableDescendants = getEditableDescendants?.(host) || {};\n        root.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        // 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 root = this.__owl__.app.createRoot(Component, {\n            props,\n            env,\n        });\n        const promise = root.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 root is\n        // mounted (but before). This will remove host children synchronously\n        // just before adding the root rendered html.\n        const fiber = root.node.fiber;\n        const fiberComplete = fiber.complete;\n        fiber.complete = function () {\n            host.replaceChildren();\n            fiberComplete.call(this);\n        };\n        const info = {\n            root,\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 { registry } from \"@web/core/registry\";\n\nexport function htmlEditorVersions() {\n    return Object.keys(registry.category(\"html_editor_upgrade\").subRegistries).sort(\n        compareVersions\n    );\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 {\n    compareVersions,\n    VERSION_SELECTOR,\n    htmlEditorVersions,\n} from \"@html_editor/html_migrations/html_migrations_utils\";\nimport { registry } from \"@web/core/registry\";\nimport { fixInvalidHTML } from \"@html_editor/utils/sanitize\";\n\n/**\n * Handle HTML transformations dependent on the current implementation of the\n * editor and its plugins for HtmlField values that were not upgraded through\n * conventional means (python upgrade script), i.e. modify obsolete\n * classes/style, convert deprecated Knowledge Behaviors to their\n * EmbeddedComponent counterparts, ...\n *\n * How to use:\n * - Create a file to export a `upgrade(element, env)` function which applies\n *   the necessary modifications inside `element` related to a specific version:\n *    - HTMLElement `element`: a container for the HtmlField value\n *    - Object `env`: the typical `owl` environment (can be used to check\n *      the current record data, use a service, ...).\n * !!!  ALWAYS assume that the `env` may not have the resource used in your\n *      upgrade function and adjust accordingly.\n * - Refer to that file in the `html_editor_upgrade` registry, in the version\n *   category related to your change: `major.minor` (bump major for an IMP,\n *   minor for a FIX), in a sub-category related to your module.\n *   Example for the version 1.1 in `html_editor`:\n *   `registry\n *        .category(\"html_editor_upgrade\")\n *        .category(\"1.1\")\n *        .add(\"html_editor\", \"@html_editor/html_migrations/migration-1.1\")`\n */\nexport class HtmlUpgradeManager {\n    constructor() {\n        this.upgradeRegistry = registry.category(\"html_editor_upgrade\");\n        this.parser = new DOMParser();\n        this.originalValue = undefined;\n        this.upgradedValue = undefined;\n        this.element = undefined;\n        this.env = {};\n    }\n\n    get value() {\n        return this.upgradedValue;\n    }\n\n    processForUpgrade(value, { containsComplexHTML, env } = {}) {\n        this.env = env || {};\n        this.containsComplexHTML = containsComplexHTML;\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.element = this.parser.parseFromString(fixInvalidHTML(value.toString()), \"text/html\")[\n            this.containsComplexHTML ? \"documentElement\" : \"body\"\n        ];\n        const versionNode = this.element.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.element, this.env);\n            }\n        }\n        return this.element[this.containsComplexHTML ? \"outerHTML\" : \"innerHTML\"];\n    }\n}\n", "import { registry } from \"@web/core/registry\";\n\n// Remove the Excalidraw EmbeddedComponent and replace it with a link.\nregistry\n    .category(\"html_editor_upgrade\")\n    .category(\"1.1\")\n    .add(\"html_editor\", \"@html_editor/html_migrations/migration-1.1\");\n", "/**\n * Remove the Excalidraw EmbeddedComponent and replace it with a link\n *\n * @param {HTMLElement} container\n * @param {Object} env\n */\nexport function upgrade(container) {\n    const excalidrawContainers = container.querySelectorAll(\"[data-embedded='draw']\");\n    for (const excalidrawContainer of excalidrawContainers) {\n        const source = JSON.parse(excalidrawContainer.dataset.embeddedProps).source;\n        const newParagraph = document.createElement(\"P\");\n        const anchor = document.createElement(\"A\");\n        newParagraph.append(anchor);\n        anchor.append(document.createTextNode(source));\n        anchor.href = source;\n        excalidrawContainer.replaceWith(newParagraph);\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { MainComponentsContainer } from \"@web/core/main_components_container\";\nimport { useForwardRefToParent } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\nimport { useRegistry } from \"@web/core/registry_hook\";\n\n/**\n * TODO ABD: refactor to propagate a reactive object instead of using a registry with an identifier\n */\nexport class LocalOverlayContainer extends MainComponentsContainer {\n    static template = \"html_editor.LocalOverlayContainer\";\n    static props = {\n        localOverlay: { type: Function, optional: true },\n        identifier: { type: String, optional: true },\n    };\n    static defaultProps = {\n        identifier: \"overlay_components\",\n    };\n\n    setup() {\n        const overlayComponents = registry.category(this.props.identifier);\n        overlayComponents.addValidation({\n            Component: { validate: (c) => c.prototype instanceof Component },\n            props: { type: Object, optional: true },\n        });\n        this.Components = useRegistry(overlayComponents);\n        useForwardRefToParent(\"localOverlay\");\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { closestBlock } from \"@html_editor/utils/blocks\";\nimport { isVisibleTextNode } from \"@html_editor/utils/dom_info\";\n\nexport class AlignPlugin extends Plugin {\n    static id = \"align\";\n    static dependencies = [\"selection\"];\n    resources = {\n        user_commands: [\n            { id: \"alignLeft\", run: () => this.align(\"left\") },\n            { id: \"alignRight\", run: () => this.align(\"right\") },\n            { id: \"alignCenter\", run: () => this.align(\"center\") },\n            { id: \"justify\", run: () => this.align(\"justify\") },\n        ],\n    };\n\n    align(mode) {\n        const visitedBlocks = new Set();\n        const traversedNode = this.dependencies.selection.getTraversedNodes();\n        for (const node of traversedNode) {\n            if (isVisibleTextNode(node)) {\n                const block = closestBlock(node);\n                if (!visitedBlocks.has(block)) {\n                    // todo @phoenix: check if it s correct in right to left ?\n                    let textAlign = getComputedStyle(block).textAlign;\n                    textAlign = textAlign === \"start\" ? \"left\" : textAlign;\n                    if (textAlign !== mode && block.isContentEditable) {\n                        block.style.textAlign = mode;\n                    }\n                    visitedBlocks.add(block);\n                }\n            }\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { fillShrunkPhrasingParent } from \"@html_editor/utils/dom\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { parseHTML } from \"@html_editor/utils/html\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nfunction isAvailable(selection) {\n    return !closestElement(selection.anchorNode, \".o_editor_banner\");\n}\nexport class BannerPlugin extends Plugin {\n    static id = \"banner\";\n    static dependencies = [\"baseContainer\", \"history\", \"dom\", \"emoji\", \"selection\", \"sanitize\"];\n    resources = {\n        normalize_handlers: this.normalize.bind(this),\n        user_commands: [\n            {\n                id: \"banner_info\",\n                title: _t(\"Banner Info\"),\n                description: _t(\"Insert an info banner\"),\n                icon: \"fa-info-circle\",\n                isAvailable,\n                run: () => {\n                    this.insertBanner(_t(\"Banner Info\"), \"\ud83d\udca1\", \"info\");\n                },\n            },\n            {\n                id: \"banner_success\",\n                title: _t(\"Banner Success\"),\n                description: _t(\"Insert a success banner\"),\n                icon: \"fa-check-circle\",\n                isAvailable,\n                run: () => {\n                    this.insertBanner(_t(\"Banner Success\"), \"\u2705\", \"success\");\n                },\n            },\n            {\n                id: \"banner_warning\",\n                title: _t(\"Banner Warning\"),\n                description: _t(\"Insert a warning banner\"),\n                icon: \"fa-exclamation-triangle\",\n                isAvailable,\n                run: () => {\n                    this.insertBanner(_t(\"Banner Warning\"), \"\u26a0\ufe0f\", \"warning\");\n                },\n            },\n            {\n                id: \"banner_danger\",\n                title: _t(\"Banner Danger\"),\n                description: _t(\"Insert a danger banner\"),\n                icon: \"fa-exclamation-circle\",\n                isAvailable,\n                run: () => {\n                    this.insertBanner(_t(\"Banner Danger\"), \"\u274c\", \"danger\");\n                },\n            },\n        ],\n        powerbox_categories: withSequence(20, { id: \"banner\", name: _t(\"Banner\") }),\n        powerbox_items: [\n            {\n                commandId: \"banner_info\",\n                categoryId: \"banner\",\n            },\n            {\n                commandId: \"banner_success\",\n                categoryId: \"banner\",\n            },\n            {\n                commandId: \"banner_warning\",\n                categoryId: \"banner\",\n            },\n            {\n                commandId: \"banner_danger\",\n                categoryId: \"banner\",\n            },\n        ],\n        power_buttons_visibility_predicates: ({ anchorNode }) =>\n            !closestElement(anchorNode, \".o_editor_banner\"),\n    };\n\n    setup() {\n        this.addDomListener(this.editable, \"click\", (e) => {\n            if (e.target.classList.contains(\"o_editor_banner_icon\")) {\n                this.onBannerEmojiChange(e.target);\n            }\n        });\n    }\n\n    insertBanner(title, emoji, alertClass) {\n        const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n        fillShrunkPhrasingParent(baseContainer);\n        const baseContainerHtml = baseContainer.outerHTML;\n        const bannerElement = parseHTML(\n            this.document,\n            `<div class=\"o_editor_banner user-select-none o_not_editable lh-1 d-flex align-items-center alert alert-${alertClass} pb-0 pt-3\" role=\"status\">\n                <i class=\"o_editor_banner_icon mb-3 fst-normal\" aria-label=\"${title}\">${emoji}</i>\n                <div class=\"w-100 px-3 o_editable\">\n                    ${baseContainerHtml}\n                </div>\n            </div`\n        ).childNodes[0];\n        this.dependencies.dom.insert(bannerElement);\n        // If the first child of editable is contenteditable false element\n        // a chromium bug prevents selecting the container.\n        // Add a paragraph above it so it's no longer the first child.\n        if (this.editable.firstChild === bannerElement) {\n            const p = this.document.createElement(\"p\");\n            p.append(this.document.createElement(\"br\"));\n            bannerElement.before(p);\n        }\n        const baseContainerName = this.dependencies.baseContainer.getDefaultNodeName();\n        this.dependencies.selection.setCursorStart(\n            bannerElement.querySelector(`.o_editor_banner > div > ${baseContainerName}`)\n        );\n        this.dependencies.history.addStep();\n    }\n\n    onBannerEmojiChange(iconElement) {\n        this.dependencies.emoji.showEmojiPicker({\n            target: iconElement,\n            onSelect: (emoji) => {\n                iconElement.textContent = emoji;\n                this.dependencies.history.addStep();\n            },\n        });\n    }\n\n    normalize(root) {\n        this.dependencies.sanitize.restoreSanitizedContentEditable(root);\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { useState } from \"@odoo/owl\";\nimport { ChatGPTDialog } from \"./chatgpt_dialog\";\n\nexport const DEFAULT_ALTERNATIVES_MODES = {\n    correct: _t(\"Correct\"),\n    short: _t(\"Shorten\"),\n    long: _t(\"Lengthen\"),\n    friendly: _t(\"Friendly\"),\n    professional: _t(\"Professional\"),\n    persuasive: _t(\"Persuasive\"),\n};\n\nlet messageId = 0;\nlet nextBatchId = 0;\n\nexport class ChatGPTAlternativesDialog extends ChatGPTDialog {\n    static template = \"html_editor.ChatGPTAlternativesDialog\";\n    static props = {\n        ...super.props,\n        originalText: String,\n        alternativesModes: { type: Object, optional: true },\n        numberOfAlternatives: { type: Number, optional: true },\n    };\n    static defaultProps = {\n        ...super.defaultProps,\n        alternativesModes: DEFAULT_ALTERNATIVES_MODES,\n        numberOfAlternatives: 3,\n    };\n\n    setup() {\n        super.setup();\n        this.state = useState({\n            ...this.state,\n            conversationHistory: [\n                {\n                    role: \"system\",\n                    content:\n                        \"The user wrote the following text:\\n\" +\n                        \"<generated_text>\" +\n                        this.props.originalText +\n                        \"</generated_text>\\n\" +\n                        \"Your goal is to help the user write alternatives to that text.\\n\" +\n                        \"Conditions:\\n\" +\n                        \"- You must respect the format (wrapping the alternative between <generated_text> and </generated_text>)\\n\" +\n                        \"- You must detect the language of the text given to you and respond in that language\\n\" +\n                        \"- Do not write HTML\\n\" +\n                        \"- You must suggest one and only one alternative per answer\\n\" +\n                        \"- Your answer must be different every time, never repeat yourself\\n\" +\n                        \"- You must respect whatever extra conditions the user gives you\\n\",\n                },\n            ],\n            messages: [],\n            alternativesMode: \"\",\n            messagesInProgress: 0,\n            currentBatchId: null,\n        });\n        this._generationIndex = 0;\n        this.generateAlternatives();\n    }\n\n    switchAlternativesMode(ev) {\n        this.state.alternativesMode = ev.currentTarget.getAttribute(\"data-mode\");\n        this.generateAlternatives(1);\n    }\n\n    async generateAlternatives(numberOfAlternatives = this.props.numberOfAlternatives) {\n        this.state.messagesInProgress = numberOfAlternatives;\n        const batchId = nextBatchId++;\n        this.state.currentBatchId = batchId;\n        let wasError = false;\n        let messageIndex = 0;\n        while (\n            !wasError &&\n            messageIndex < numberOfAlternatives &&\n            this.state.currentBatchId === batchId\n        ) {\n            this._generationIndex += 1;\n            let query = messageIndex\n                ? \"Write one alternative version of the original text.\"\n                : \"Try again another single version of the original text.\";\n            if (this.state.alternativesMode && !messageIndex) {\n                query += ` Make it more ${this.state.alternativesMode} than your last answer.`;\n            }\n            if (this.state.alternativesMode === \"correct\") {\n                query =\n                    \"Simply correct the text, without altering its meaning in any way. Preserve whatever language the user wrote their text in.\";\n            }\n            await this.generate(query, (content, isError) => {\n                if (this.state.currentBatchId === batchId) {\n                    const alternative = content\n                        .replace(/^[\\s\\S]*<generated_text>/, \"\")\n                        .replace(/<\\/generated_text>[\\s\\S]*$/, \"\");\n                    if (isError) {\n                        wasError = true;\n                    } else {\n                        this.state.conversationHistory.push(\n                            {\n                                role: \"user\",\n                                content: query,\n                            },\n                            {\n                                role: \"assistant\",\n                                content,\n                            }\n                        );\n                    }\n                    this.state.messages.push({\n                        author: \"assistant\",\n                        text: alternative,\n                        isError,\n                        batchId,\n                        mode: this.state.alternativesMode,\n                        id: messageId++,\n                    });\n                }\n            }).catch(() => {\n                if (this.state.currentBatchId === batchId) {\n                    wasError = true;\n                    this.state.messages = [];\n                }\n            });\n            messageIndex += 1;\n            this.state.messagesInProgress -= 1;\n            if (wasError) {\n                break;\n            }\n        }\n        this.state.messagesInProgress = 0;\n    }\n\n    preventDialogMousedown(ev) {\n        // Prevent the default behavior of a mousedown event on the dialog\n        // itself so it doesn't cancel the user's text selection in the editor.\n        ev.preventDefault();\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, useState, onWillDestroy, status, markup } from \"@odoo/owl\";\n\nconst POSTPROCESS_GENERATED_CONTENT = (content, baseContainer) => {\n    let lines = content.split(\"\\n\");\n    if (baseContainer.toUpperCase() === \"P\") {\n        // P has a margin bottom which is used as an interline, no need to\n        // keep empty lines in that case.\n        lines = lines.filter((line) => line.trim().length);\n    }\n    const fragment = document.createDocumentFragment();\n    let parentUl, parentOl;\n    let lineIndex = 0;\n    for (const line of lines) {\n        if (line.trim().startsWith(\"- \")) {\n            // Create or continue an unordered list.\n            parentUl = parentUl || document.createElement(\"ul\");\n            const li = document.createElement(\"li\");\n            li.innerText = line.trim().slice(2);\n            parentUl.appendChild(li);\n        } else if (\n            (parentOl && line.startsWith(`${parentOl.children.length + 1}. `)) ||\n            (!parentOl && line.startsWith(\"1. \") && lines[lineIndex + 1]?.startsWith(\"2. \"))\n        ) {\n            // Create or continue an ordered list (only if the line starts\n            // with the next number in the current ordered list (or 1 if no\n            // ordered list was in progress and it's followed by a 2).\n            parentOl = parentOl || document.createElement(\"ol\");\n            const li = document.createElement(\"li\");\n            li.innerText = line.slice(line.indexOf(\".\") + 2);\n            parentOl.appendChild(li);\n        } else if (line.trim().length === 0) {\n            const emptyLine = document.createElement(\"DIV\");\n            emptyLine.append(document.createElement(\"BR\"));\n            fragment.appendChild(emptyLine);\n        } else {\n            // Insert any list in progress, and a new block for the current\n            // line.\n            [parentUl, parentOl].forEach((list) => list && fragment.appendChild(list));\n            parentUl = parentOl = undefined;\n            const block = document.createElement(line.startsWith(\"Title: \") ? \"h2\" : baseContainer);\n            block.innerText = line;\n            fragment.appendChild(block);\n        }\n        lineIndex += 1;\n    }\n    [parentUl, parentOl].forEach((list) => list && fragment.appendChild(list));\n    return fragment;\n};\n\nexport class ChatGPTDialog extends Component {\n    static template = \"\";\n    static components = { Dialog };\n    static props = {\n        insert: { type: Function },\n        close: { type: Function },\n        sanitize: { type: Function },\n        baseContainer: { type: String, optional: true },\n    };\n    static defaultProps = {\n        baseContainer: \"DIV\",\n    };\n\n    setup() {\n        this.notificationService = useService(\"notification\");\n        this.state = useState({ selectedMessageId: null });\n        onWillDestroy(() => this.pendingRpcPromise?.abort());\n    }\n\n    selectMessage(ev) {\n        this.state.selectedMessageId = +ev.currentTarget.getAttribute(\"data-message-id\");\n    }\n\n    insertMessage(ev) {\n        this.selectMessage(ev);\n        this._confirm();\n    }\n\n    formatContent(content) {\n        const fragment = POSTPROCESS_GENERATED_CONTENT(content, this.props.baseContainer);\n        let result = \"\";\n        for (const child of fragment.children) {\n            this.props.sanitize(child, { IN_PLACE: true });\n            result += child.outerHTML;\n        }\n        return markup(result);\n    }\n\n    generate(prompt, callback) {\n        const protectedCallback = (...args) => {\n            if (status(this) !== \"destroyed\") {\n                delete this.pendingRpcPromise;\n                return callback(...args);\n            }\n        };\n        this.pendingRpcPromise = rpc(\n            \"/html_editor/generate_text\",\n            {\n                prompt,\n                conversation_history: this.state.conversationHistory,\n            },\n            { shadow: true }\n        );\n        return this.pendingRpcPromise\n            .then((content) => protectedCallback(content))\n            .catch((error) => protectedCallback(_t(error.data?.message || error.message), true));\n    }\n\n    _cancel() {\n        this.props.close();\n    }\n\n    _confirm() {\n        try {\n            this.props.close();\n            const text = this.state.messages.find(\n                (message) => message.id === this.state.selectedMessageId\n            )?.text;\n            this.notificationService.add(_t(\"Your content was successfully generated.\"), {\n                title: _t(\"Content generated\"),\n                type: \"success\",\n            });\n            const fragment = POSTPROCESS_GENERATED_CONTENT(text || \"\", this.props.baseContainer);\n            this.props.sanitize(fragment, { IN_PLACE: true });\n            this.props.insert(fragment);\n        } catch (e) {\n            this.props.close();\n            throw e;\n        }\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { closestElement } from \"../../utils/dom_traversal\";\nimport { ChatGPTPromptDialog } from \"./chatgpt_prompt_dialog\";\nimport { ChatGPTAlternativesDialog } from \"./chatgpt_alternatives_dialog\";\nimport { ChatGPTTranslateDialog } from \"./chatgpt_translate_dialog\";\nimport { LanguageSelector } from \"./language_selector\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { user } from \"@web/core/user\";\nimport { isContentEditable } from \"@html_editor/utils/dom_info\";\n\nexport class ChatGPTPlugin extends Plugin {\n    static id = \"chatgpt\";\n    static dependencies = [\n        \"baseContainer\",\n        \"selection\",\n        \"history\",\n        \"dom\",\n        \"sanitize\",\n        \"dialog\",\n        \"split\",\n    ];\n    resources = {\n        user_commands: [\n            {\n                id: \"openChatGPTDialog\",\n                title: _t(\"ChatGPT\"),\n                description: _t(\"Generate or transform content with AI.\"),\n                icon: \"fa-magic\",\n                run: this.openDialog.bind(this),\n            },\n        ],\n        toolbar_groups: withSequence(50, {\n            id: \"ai\",\n        }),\n        toolbar_items: [\n            {\n                id: \"translate\",\n                groupId: \"ai\",\n                title: _t(\"Translate with AI\"),\n                isAvailable: (selection) => {\n                    return !selection.isCollapsed && user.userId;\n                },\n                isDisabled: this.isNotReplaceableByAI.bind(this),\n                Component: LanguageSelector,\n                props: {\n                    onSelected: (language) => this.openDialog({ language }),\n                    isDisabled: (selection) => {\n                        return this.isNotReplaceableByAI(selection);\n                    },\n                },\n            },\n            {\n                id: \"chatgpt\",\n                groupId: \"ai\",\n                commandId: \"openChatGPTDialog\",\n                text: \"AI\",\n                isDisabled: this.isNotReplaceableByAI.bind(this),\n            },\n        ],\n\n        powerbox_categories: withSequence(70, { id: \"ai\", name: _t(\"AI Tools\") }),\n        powerbox_items: {\n            keywords: [_t(\"AI\")],\n            categoryId: \"ai\",\n            commandId: \"openChatGPTDialog\",\n            // isAvailable: () => !this.odooEditor.isSelectionInBlockRoot(), // TODO!\n        },\n    };\n\n    isNotReplaceableByAI(selection = this.dependencies.selection.getEditableSelection()) {\n        const isEmpty = !selection.textContent().replace(/\\s+/g, \"\");\n        const cannotReplace = [...selection.commonAncestorContainer.childNodes].find(\n            (el) => this.dependencies.split.isUnsplittable(el) || !isContentEditable(el)\n        );\n        return cannotReplace || isEmpty;\n    }\n\n    openDialog(params = {}) {\n        const selection = this.dependencies.selection.getEditableSelection();\n        const dialogParams = {\n            insert: (content) => {\n                const insertedNodes = this.dependencies.dom.insert(content);\n                this.dependencies.history.addStep();\n                // Add a frame around the inserted content to highlight it for 2\n                // seconds.\n                const start = insertedNodes?.length && closestElement(insertedNodes[0]);\n                const end =\n                    insertedNodes?.length &&\n                    closestElement(insertedNodes[insertedNodes.length - 1]);\n                if (start && end) {\n                    const divContainer = this.editable.parentElement;\n                    let [parent, left, top] = [\n                        start.offsetParent,\n                        start.offsetLeft,\n                        start.offsetTop - start.scrollTop,\n                    ];\n                    while (parent && !parent.contains(divContainer)) {\n                        left += parent.offsetLeft;\n                        top += parent.offsetTop - parent.scrollTop;\n                        parent = parent.offsetParent;\n                    }\n                    let [endParent, endTop] = [end.offsetParent, end.offsetTop - end.scrollTop];\n                    while (endParent && !endParent.contains(divContainer)) {\n                        endTop += endParent.offsetTop - endParent.scrollTop;\n                        endParent = endParent.offsetParent;\n                    }\n                    const div = document.createElement(\"div\");\n                    div.classList.add(\"o-chatgpt-content\");\n                    const FRAME_PADDING = 3;\n                    div.style.left = `${left - FRAME_PADDING}px`;\n                    div.style.top = `${top - FRAME_PADDING}px`;\n                    div.style.width = `${\n                        Math.max(start.offsetWidth, end.offsetWidth) + FRAME_PADDING * 2\n                    }px`;\n                    div.style.height = `${endTop + end.offsetHeight - top + FRAME_PADDING * 2}px`;\n                    divContainer.prepend(div);\n                    setTimeout(() => div.remove(), 2000);\n                }\n            },\n            ...params,\n        };\n        dialogParams.baseContainer = this.dependencies.baseContainer.getDefaultNodeName();\n        // collapse to end\n        const sanitize = this.dependencies.sanitize.sanitize;\n        if (selection.isCollapsed) {\n            this.dependencies.dialog.addDialog(ChatGPTPromptDialog, { ...dialogParams, sanitize });\n        } else {\n            const originalText = selection.textContent() || \"\";\n            this.dependencies.dialog.addDialog(\n                params.language ? ChatGPTTranslateDialog : ChatGPTAlternativesDialog,\n                { ...dialogParams, originalText, sanitize }\n            );\n        }\n        if (this.services.ui.isSmall) {\n            // TODO: Find a better way and avoid modifying range\n            // HACK: In the case of opening through dropdown:\n            // - when dropdown open, it keep the element focused before the open\n            // - when opening the dialog through the dropdown, the dropdown closes\n            // - upon close, the generic code of the dropdown sets focus on the kept element (in our case, the editable)\n            // - we need to remove the range after the generic code of the dropdown is triggered so we hack it by removing the range in the next tick\n            Promise.resolve().then(() => {\n                // If the dialog is opened on a small screen, remove all selection\n                // because the selection can be seen through the dialog on some devices.\n                this.document.getSelection()?.removeAllRanges();\n            });\n        }\n    }\n}\n", "import { browser } from \"@web/core/browser/browser\";\nimport { user } from \"@web/core/user\";\nimport { useAutofocus, useChildRef } from \"@web/core/utils/hooks\";\nimport { useState, useEffect, useRef } from \"@odoo/owl\";\nimport { ChatGPTDialog } from \"./chatgpt_dialog\";\nimport { scrollTo } from \"@web/core/utils/scrolling\";\n\nexport class ChatGPTPromptDialog extends ChatGPTDialog {\n    static template = \"html_editor.ChatGPTPromptDialog\";\n    static props = {\n        ...super.props,\n        initialPrompt: { type: String, optional: true },\n    };\n    static defaultProps = {\n        ...super.defaultProps,\n        initialPrompt: \"\",\n    };\n\n    setup() {\n        super.setup();\n        this.assistantAvatarUrl = `${browser.location.origin}/web_editor/static/src/img/odoobot_transparent.png`;\n        this.userAvatarUrl = `${\n            browser.location.origin\n        }/web/image?model=res.users&field=avatar_128&id=${encodeURIComponent(user.userId)}`;\n        this.state = useState({\n            ...this.state,\n            prompt: this.props.initialPrompt,\n            conversationHistory: [\n                {\n                    role: \"system\",\n                    content:\n                        \"You are a helpful assistant, your goal is to help the user write their document.\",\n                },\n                {\n                    role: \"assistant\",\n                    content: \"What do you need ?\",\n                },\n            ],\n            messages: [],\n        });\n        this.promptInputRef = useRef(\"promptInput\");\n        this.modalRef = useChildRef();\n        useAutofocus({ refName: \"promptInput\", mobile: true });\n        useEffect(\n            () => {\n                // Resize the textarea to fit its content.\n                this.promptInputRef.el.style.height = 0;\n                this.promptInputRef.el.style.height = this.promptInputRef.el.scrollHeight + \"px\";\n            },\n            () => [this.state.prompt]\n        );\n        useEffect(\n            () => {\n                // Scroll to the latest message whenever new message\n                // is inserted.\n                const modalEl = this.modalRef.el.querySelector(\"main.modal-body\");\n                const lastMessageEl = modalEl.lastElementChild;\n                scrollTo(lastMessageEl, {\n                    behavior: \"smooth\",\n                    isAnchor: true,\n                });\n            },\n            () => [this.state.conversationHistory.length]\n        );\n    }\n\n    onTextareaKeydown(ev) {\n        if (ev.key === \"Enter\" && !ev.shiftKey) {\n            ev.stopImmediatePropagation();\n            if (this.state.prompt.trim().length) {\n                this.submitPrompt(ev);\n            }\n        }\n    }\n\n    submitPrompt(ev) {\n        this.freezeInput();\n        ev.preventDefault();\n        const prompt = this.state.prompt;\n        this.state.messages.push({ author: \"user\", text: prompt });\n        const messageId = new Date().getTime();\n        const conversation = { role: \"user\", content: prompt };\n        this.state.conversationHistory.push(conversation);\n        this.state.messages.push({ author: \"assistant\", id: messageId });\n        this.state.prompt = \"\";\n        this.generate(prompt, (content, isError) => {\n            if (isError) {\n                // There was an error, remove the prompt from the history.\n                this.state.conversationHistory = this.state.conversationHistory.filter(\n                    (c) => c !== conversation\n                );\n            } else {\n                // There was no error, add the response to the history.\n                this.state.conversationHistory.push({ role: \"assistant\", content });\n            }\n            const messageIndex = this.state.messages.findIndex((m) => m.id === messageId);\n            this.state.messages[messageIndex] = {\n                author: \"assistant\",\n                text: content,\n                isError,\n                id: messageId,\n            };\n            this.unfreezeInput();\n        });\n    }\n\n    freezeInput() {\n        this.promptInputRef.el.setAttribute(\"disabled\", \"\");\n    }\n\n    unfreezeInput() {\n        this.promptInputRef.el.removeAttribute(\"disabled\");\n        this.promptInputRef.el.focus();\n    }\n\n    /**\n     * @override\n     */\n    _cancel() {\n        this.freezeInput();\n        super._cancel();\n    }\n\n    /**\n     * @override\n     */\n    _confirm() {\n        this.freezeInput();\n        super._confirm();\n    }\n}\n", "import { useState } from \"@odoo/owl\";\nimport { ChatGPTDialog } from \"./chatgpt_dialog\";\n\nexport class ChatGPTTranslateDialog extends ChatGPTDialog {\n    static template = \"html_editor.ChatGPTTranslateDialog\";\n    static props = {\n        ...super.props,\n        originalText: String,\n        language: String,\n    };\n\n    setup() {\n        super.setup();\n        this.state = useState({\n            ...this.state,\n            conversationHistory: [\n                {\n                    role: \"system\",\n                    content:\n                        \"You are a translation assistant. You goal is to translate text while maintaining the original format and\" +\n                        \"respecting specific instructions. \\n\" +\n                        \"Instructions: \\n\" +\n                        \"- You must respect the format (wrapping the translated text between <generated_text> and </generated_text>)\\n\" +\n                        \"- Do not write HTML.\",\n                },\n            ],\n            messages: [],\n            translationInProgress: true,\n        });\n        this.translate();\n    }\n\n    async translate() {\n        const query = `Translate <generated_text>${this.props.originalText}</generated_text> to ${this.props.language}`;\n        const messageId = new Date().getTime();\n        await this.generate(query, (content, isError) => {\n            let translatedText = content\n                .replace(/^[\\s\\S]*<generated_text>/, \"\")\n                .replace(/<\\/generated_text>[\\s\\S]*$/, \"\");\n            if (!this.formatContent(translatedText).length) {\n                isError = true;\n                translatedText = \"You didn't select any text.\";\n            }\n            this.state.translationInProgress = false;\n            if (!isError) {\n                // There was no error, add the response to the history.\n                this.state.conversationHistory.push(\n                    {\n                        role: \"user\",\n                        content: query,\n                    },\n                    {\n                        role: \"assistant\",\n                        content,\n                    }\n                );\n            }\n            this.state.messages.push({\n                author: \"assistant\",\n                text: translatedText,\n                id: messageId,\n                isError,\n            });\n            this.state.selectedMessageId = messageId;\n        });\n    }\n}\n", "import { Component, onWillStart, useState } 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 { loadLanguages } from \"@web/core/l10n/translation\";\nimport { toolbarButtonProps } from \"@html_editor/main/toolbar/toolbar\";\nimport { user } from \"@web/core/user\";\n\nexport class LanguageSelector extends Component {\n    static template = \"html_editor.LanguageSelector\";\n    static props = {\n        ...toolbarButtonProps,\n        onSelected: { type: Function },\n        isDisabled: { type: Function, optional: true },\n    };\n    static components = { Dropdown, DropdownItem };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.state = useState({\n            languages: [],\n        });\n        onWillStart(() => {\n            if (user.userId) {\n                loadLanguages(this.orm).then((res) => {\n                    this.state.languages = res;\n                });\n            }\n        });\n    }\n    onSelected(language) {\n        this.props.onSelected(language);\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { closestBlock } from \"@html_editor/utils/blocks\";\nimport { unwrapContents } from \"@html_editor/utils/dom\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\n\nconst REGEX_BOOTSTRAP_COLUMN = /(?:^| )col(-[a-zA-Z]+)?(-\\d+)?(?:$| )/;\n\nfunction isUnremovableColumn(node, root) {\n    const isColumnInnerStructure =\n        node.nodeName === \"DIV\" && [...node.classList].some((cls) => /^row$|^col$|^col-/.test(cls));\n\n    if (!isColumnInnerStructure) {\n        return false;\n    }\n    if (!root) {\n        return true;\n    }\n    const closestColumnContainer = closestElement(node, \"div.o_text_columns\");\n    return !root.contains(closestColumnContainer);\n}\n\nfunction columnIsAvailable(numberOfColumns) {\n    return (selection) => {\n        const row = closestElement(selection.anchorNode, \".o_text_columns .row\");\n        return !(row && row.childElementCount === numberOfColumns);\n    };\n}\n\nexport class ColumnPlugin extends Plugin {\n    static id = \"column\";\n    static dependencies = [\"baseContainer\", \"selection\", \"history\"];\n    resources = {\n        user_commands: [\n            {\n                id: \"columnize\",\n                title: _t(\"Columnize\"),\n                description: _t(\"Convert into columns\"),\n                icon: \"fa-columns\",\n                run: this.columnize.bind(this),\n            },\n        ],\n        powerbox_items: [\n            {\n                title: _t(\"2 columns\"),\n                description: _t(\"Convert into 2 columns\"),\n                categoryId: \"structure\",\n                isAvailable: columnIsAvailable(2),\n                commandId: \"columnize\",\n                commandParams: { numberOfColumns: 2 },\n            },\n            {\n                title: _t(\"3 columns\"),\n                description: _t(\"Convert into 3 columns\"),\n                categoryId: \"structure\",\n                isAvailable: columnIsAvailable(3),\n                commandId: \"columnize\",\n                commandParams: { numberOfColumns: 3 },\n            },\n            {\n                title: _t(\"4 columns\"),\n                description: _t(\"Convert into 4 columns\"),\n                categoryId: \"structure\",\n                isAvailable: columnIsAvailable(4),\n                commandId: \"columnize\",\n                commandParams: { numberOfColumns: 4 },\n            },\n            {\n                title: _t(\"Remove columns\"),\n                description: _t(\"Back to one column\"),\n                categoryId: \"structure\",\n                isAvailable: (selection) =>\n                    !!closestElement(selection.anchorNode, \".o_text_columns .row\"),\n                commandId: \"columnize\",\n                commandParams: { numberOfColumns: 0 },\n            },\n        ],\n        hints: [\n            {\n                selector: `.odoo-editor-editable .o_text_columns div[class^='col-'],\n                            .odoo-editor-editable .o_text_columns div[class^='col-']>p:first-child`,\n                text: _t(\"Empty column\"),\n            },\n        ],\n        unremovable_node_predicates: isUnremovableColumn,\n        power_buttons_visibility_predicates: ({ anchorNode }) =>\n            !closestElement(anchorNode, \".o_text_columns\"),\n    };\n\n    columnize({ numberOfColumns, addParagraphAfter = true } = {}) {\n        const selectionToRestore = this.dependencies.selection.getEditableSelection();\n        const anchor = selectionToRestore.anchorNode;\n        const hasColumns = !!closestElement(anchor, \".o_text_columns\");\n        if (hasColumns) {\n            if (numberOfColumns) {\n                this.changeColumnsNumber(anchor, numberOfColumns);\n            } else {\n                this.removeColumns(anchor);\n            }\n        } else if (numberOfColumns) {\n            this.createColumns(anchor, numberOfColumns, addParagraphAfter);\n        }\n        this.dependencies.selection.setSelection(selectionToRestore);\n        this.dependencies.history.addStep();\n    }\n\n    removeColumns(anchor) {\n        const container = closestElement(anchor, \".o_text_columns\");\n        const rows = unwrapContents(container);\n        for (const row of rows) {\n            const columns = unwrapContents(row);\n            for (const column of columns) {\n                unwrapContents(column);\n                // const columnContents = unwrapContents(column);\n                // for (const node of columnContents) {\n                //     resetOuids(node);\n                // }\n            }\n        }\n    }\n\n    createColumns(anchor, numberOfColumns, addParagraphAfter) {\n        const container = this.document.createElement(\"div\");\n        if (!closestElement(anchor, \".container\")) {\n            container.classList.add(\"container\");\n        }\n        container.classList.add(\"o_text_columns\");\n        const row = this.document.createElement(\"div\");\n        row.classList.add(\"row\");\n        container.append(row);\n        const block = closestBlock(anchor);\n        // resetOuids(block);\n        const columnSize = Math.floor(12 / numberOfColumns);\n        const columns = [];\n        for (let i = 0; i < numberOfColumns; i++) {\n            const column = this.document.createElement(\"div\");\n            column.classList.add(`col-${columnSize}`);\n            row.append(column);\n            columns.push(column);\n        }\n        block.before(container);\n        columns.shift().append(block);\n        for (const column of columns) {\n            const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n            baseContainer.append(this.document.createElement(\"br\"));\n            column.append(baseContainer);\n        }\n        if (addParagraphAfter) {\n            const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n            baseContainer.append(this.document.createElement(\"br\"));\n            container.after(baseContainer);\n        }\n    }\n\n    changeColumnsNumber(anchor, numberOfColumns) {\n        const row = closestElement(anchor, \".row\");\n        const columns = [...row.children];\n        const columnSize = Math.floor(12 / numberOfColumns);\n        const diff = numberOfColumns - columns.length;\n        if (!diff) {\n            return;\n        }\n        for (const column of columns) {\n            column.className = column.className.replace(\n                REGEX_BOOTSTRAP_COLUMN,\n                `col$1-${columnSize}`\n            );\n        }\n        if (diff > 0) {\n            // Add extra columns.\n            let lastColumn = columns[columns.length - 1];\n            for (let i = 0; i < diff; i++) {\n                const column = this.document.createElement(\"div\");\n                column.classList.add(`col-${columnSize}`);\n                const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n                baseContainer.append(this.document.createElement(\"br\"));\n                column.append(baseContainer);\n                lastColumn.after(column);\n                lastColumn = column;\n            }\n        } else if (diff < 0) {\n            // Remove superfluous columns.\n            const contents = [];\n            for (let i = diff; i < 0; i++) {\n                const column = columns.pop();\n                const columnContents = unwrapContents(column);\n                // for (const node of columnContents) {\n                //     resetOuids(node);\n                // }\n                contents.unshift(...columnContents);\n            }\n            columns[columns.length - 1].append(...contents);\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { EmojiPicker } from \"@web/core/emoji_picker/emoji_picker\";\nimport { _t } from \"@web/core/l10n/translation\";\n\n/**\n * @typedef { Object } EmojiShared\n * @property { EmojiPlugin['showEmojiPicker'] } showEmojiPicker\n */\n\nexport class EmojiPlugin extends Plugin {\n    static id = \"emoji\";\n    static dependencies = [\"history\", \"overlay\", \"dom\", \"selection\"];\n    static shared = [\"showEmojiPicker\"];\n    resources = {\n        user_commands: [\n            {\n                id: \"addEmoji\",\n                title: _t(\"Emoji\"),\n                description: _t(\"Add an emoji\"),\n                icon: \"fa-smile-o\",\n                run: this.showEmojiPicker.bind(this),\n            },\n        ],\n        powerbox_items: [\n            {\n                categoryId: \"widget\",\n                commandId: \"addEmoji\",\n            },\n        ],\n    };\n\n    setup() {\n        this.overlay = this.dependencies.overlay.createOverlay(EmojiPicker, {\n            hasAutofocus: true,\n            className: \"popover\",\n        });\n    }\n\n    /**\n     * @param {Object} options\n     * @param {HTMLElement} options.target - The target element to position the overlay.\n     * @param {Function} [options.onSelect] - The callback function to handle the selection of an emoji.\n     * If not provided, the emoji will be inserted into the editor and a step will be trigerred.\n     */\n    showEmojiPicker({ target, onSelect } = {}) {\n        this.overlay.open({\n            props: {\n                close: () => {\n                    this.overlay.close();\n                    this.dependencies.selection.focusEditable();\n                },\n                onSelect: (str) => {\n                    if (onSelect) {\n                        onSelect(str);\n                        return;\n                    }\n                    this.dependencies.dom.insert(str);\n                    this.dependencies.history.addStep();\n                },\n            },\n            target,\n        });\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { cleanTextNode } from \"@html_editor/utils/dom\";\nimport { isTextNode, isZwnbsp } from \"@html_editor/utils/dom_info\";\nimport { prepareUpdate } from \"@html_editor/utils/dom_state\";\nimport { descendants, selectElements } from \"@html_editor/utils/dom_traversal\";\nimport { leftPos } from \"@html_editor/utils/position\";\nimport { callbacksForCursorUpdate } from \"@html_editor/utils/selection\";\n\n/** @typedef {import(\"../core/selection_plugin\").Cursors} Cursors */\n\n/**\n * @typedef { Object } FeffShared\n * @property { FeffPlugin['addFeff'] } addFeff\n * @property { FeffPlugin['removeFeffs'] } removeFeffs\n * @property { FeffPlugin['updateFeffs'] } updateFeffs\n */\n\n/**\n * This plugin manages the insertion and removal of the zero-width no-break\n * space character (U+FEFF). These characters enable the user to place the\n * cursor in positions that would otherwise not be easy or possible, such as\n * between two contenteditable=false elements, or at the end (but inside) of a\n * link.\n */\nexport class FeffPlugin extends Plugin {\n    static id = \"feff\";\n    static dependencies = [\"selection\"];\n    static shared = [\"addFeff\", \"removeFeffs\", \"updateFeffs\"];\n\n    resources = {\n        normalize_handlers: this.updateFeffs.bind(this),\n        clean_handlers: (root) => this.clean({ root, preserveSelection: true }),\n        clean_for_save_handlers: this.clean.bind(this),\n        intangible_char_for_keyboard_navigation_predicates: (ev, char, lastSkipped) =>\n            // Skip first FEFF, but not the second one (unless shift is pressed).\n            char === \"\\uFEFF\" && (ev.shiftKey || lastSkipped !== \"\\uFEFF\"),\n    };\n\n    clean({ root, preserveSelection = false }) {\n        if (preserveSelection) {\n            const cursors = this.getCursors();\n            this.removeFeffs(root, cursors);\n            cursors.restore();\n        } else {\n            this.removeFeffs(root, null);\n        }\n    }\n\n    /**\n     * @param {Element} root\n     * @param {Cursors} [cursors]\n     * @param {Object} [options]\n     */\n    removeFeffs(root, cursors, { exclude = () => false } = {}) {\n        const hasFeff = (node) =>\n            isTextNode(node) &&\n            node.textContent.includes(\"\\ufeff\") &&\n            node.parentElement.isContentEditable;\n\n        for (const node of descendants(root).filter((n) => hasFeff(n) && !exclude(n))) {\n            // Remove all FEFF within a `prepareUpdate` to make sure to make <br>\n            // nodes visible if needed.\n            const restoreSpaces = prepareUpdate(...leftPos(node));\n            cleanTextNode(node, \"\\ufeff\", cursors);\n            restoreSpaces();\n        }\n    }\n\n    /**\n     * @param {Element} element\n     * @param {'before'|'after'|'prepend'|'append'} position\n     * @param {Cursors} [cursors]\n     * @returns {Node}\n     */\n    addFeff(element, position, cursors) {\n        const feff = this.document.createTextNode(\"\\ufeff\");\n        cursors?.update(callbacksForCursorUpdate[position](element, feff));\n        element[position](feff);\n        return feff;\n    }\n\n    /**\n     * Adds a FEFF before and after each element that matches the selectors\n     * provided by the registered providers.\n     *\n     * @param {Element} root\n     * @param {Cursors} cursors\n     * @returns {Node[]}\n     */\n    padWithFeffs(root, cursors) {\n        const combinedSelector = this.getResource(\"selectors_for_feff_providers\")\n            .map((provider) => provider())\n            .join(\", \");\n        if (!combinedSelector) {\n            return [];\n        }\n        const elements = [...selectElements(root, combinedSelector)];\n        const feffNodes = elements\n            .flatMap((el) => {\n                const addFeff = (position) => this.addFeff(el, position, cursors);\n                return [\n                    isZwnbsp(el.previousSibling) ? el.previousSibling : addFeff(\"before\"),\n                    isZwnbsp(el.nextSibling) ? el.nextSibling : addFeff(\"after\"),\n                ];\n            })\n            // Avoid sequential FEFFs\n            .filter((feff, i, array) => !(i > 0 && areCloseSiblings(array[i - 1], feff)));\n        return feffNodes;\n    }\n\n    updateFeffs(root) {\n        const cursors = this.getCursors();\n        // Pad based on selectors\n        const feffNodesBasedOnSelectors = this.padWithFeffs(root, cursors);\n        // Custom feff adding\n        // Each provider is responsible for adding (or keeping) FEFF nodes and\n        // returning a list of them.\n        const customFeffNodes = this.getResource(\"feff_providers\").flatMap((p) => p(root, cursors));\n        const feffNodesToKeep = new Set([...feffNodesBasedOnSelectors, ...customFeffNodes]);\n        this.removeFeffs(root, cursors, { exclude: (node) => feffNodesToKeep.has(node) });\n        cursors.restore();\n    }\n\n    /**\n     * Retuns a patched version of cursors in which `restore` does nothing\n     * unless `update` has been called at least once.\n     */\n    getCursors() {\n        const cursors = this.dependencies.selection.preserveSelection();\n        const originalUpdate = cursors.update.bind(cursors);\n        const originalRestore = cursors.restore.bind(cursors);\n        let shouldRestore = false;\n        cursors.update = (...args) => {\n            shouldRestore = true;\n            return originalUpdate(...args);\n        };\n        cursors.restore = () => {\n            if (shouldRestore) {\n                originalRestore();\n            }\n        };\n        return cursors;\n    }\n}\n\n/**\n * Whether two nodes are consecutive siblings, ignoring empty text nodes between\n * them.\n *\n * @param {Node} a\n * @param {Node} b\n */\nfunction areCloseSiblings(a, b) {\n    let next = a.nextSibling;\n    // skip empty text nodes\n    while (next && isTextNode(next) && !next.textContent) {\n        next = next.nextSibling;\n    }\n    return next === b;\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport {\n    isColorGradient,\n    rgbToHex,\n    hasColor,\n    hasAnyNodesColor,\n    TEXT_CLASSES_REGEX,\n    BG_CLASSES_REGEX,\n    RGBA_REGEX,\n} from \"@html_editor/utils/color\";\nimport { fillEmpty } from \"@html_editor/utils/dom\";\nimport {\n    isContentEditable,\n    isEmptyBlock,\n    isTextNode,\n    isWhitespace,\n    isZwnbsp,\n} from \"@html_editor/utils/dom_info\";\nimport { closestElement, descendants } from \"@html_editor/utils/dom_traversal\";\nimport { isCSSColor } from \"@web/core/utils/colors\";\nimport { ColorSelector } from \"./color_selector\";\nimport { reactive } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { withSequence } from \"@html_editor/utils/resource\";\n\nconst RGBA_OPACITY = 0.6;\nconst HEX_OPACITY = \"99\";\n\n/**\n * @typedef { Object } ColorShared\n * @property { ColorPlugin['colorElement'] } colorElement\n * @property { ColorPlugin['getPropsForColorSelector'] } getPropsForColorSelector\n */\nexport class ColorPlugin extends Plugin {\n    static id = \"color\";\n    static dependencies = [\"selection\", \"split\", \"history\", \"format\"];\n    static shared = [\"colorElement\", \"getPropsForColorSelector\"];\n    resources = {\n        user_commands: [\n            {\n                id: \"applyColor\",\n                run: this.applyColor.bind(this),\n            },\n        ],\n        toolbar_groups: withSequence(25, {\n            id: \"color\",\n        }),\n        toolbar_items: [\n            {\n                id: \"forecolor\",\n                groupId: \"color\",\n                title: _t(\"Font Color\"),\n                Component: ColorSelector,\n                props: this.getPropsForColorSelector(\"foreground\"),\n            },\n            {\n                id: \"backcolor\",\n                groupId: \"color\",\n                title: _t(\"Background Color\"),\n                Component: ColorSelector,\n                props: this.getPropsForColorSelector(\"background\"),\n            },\n        ],\n\n        /** Handlers */\n        selectionchange_handlers: this.updateSelectedColor.bind(this),\n        remove_format_handlers: this.removeAllColor.bind(this),\n    };\n\n    setup() {\n        this.selectedColors = reactive({ color: \"\", backgroundColor: \"\" });\n        this.previewableApplyColor = this.dependencies.history.makePreviewableOperation(\n            (color, mode, previewMode) => this._applyColor(color, mode, previewMode)\n        );\n    }\n\n    /**\n     * @param {'foreground'|'background'} type\n     */\n    getPropsForColorSelector(type) {\n        const mode = type === \"foreground\" ? \"color\" : \"backgroundColor\";\n        return {\n            type,\n            getUsedCustomColors: () => this.getUsedCustomColors(mode),\n            getSelectedColors: () => this.selectedColors,\n            applyColor: this.applyColor.bind(this),\n            applyColorPreview: this.applyColorPreview.bind(this),\n            applyColorResetPreview: this.applyColorResetPreview.bind(this),\n            focusEditable: () => this.dependencies.selection.focusEditable(),\n        };\n    }\n\n    updateSelectedColor() {\n        const nodes = this.dependencies.selection.getTraversedNodes().filter(isTextNode);\n        if (nodes.length === 0) {\n            return;\n        }\n        const el = closestElement(nodes[0]);\n        if (!el) {\n            return;\n        }\n        const elStyle = getComputedStyle(el);\n        const backgroundImage = elStyle.backgroundImage;\n        const hasGradient = isColorGradient(backgroundImage);\n        const hasTextGradientClass = el.classList.contains(\"text-gradient\");\n\n        let backgroundColor = elStyle.backgroundColor;\n        const activeTab = document\n            .querySelector(\".o_font_color_selector button.active\")\n            ?.innerHTML.trim();\n        if (backgroundColor.startsWith(\"rgba\") && activeTab === \"Solid\") {\n            // Buttons in the solid tab of color selector have no\n            // opacity, hence to match selected color correctly,\n            // we need to remove applied 0.6 opacity.\n            const values = backgroundColor.match(RGBA_REGEX) || [];\n            const alpha = parseFloat(values.pop()); // Extract alpha value\n            if (alpha === RGBA_OPACITY) {\n                backgroundColor = `rgb(${values.slice(0, 3).join(\", \")})`; // Remove alpha\n            }\n        }\n\n        this.selectedColors.color =\n            hasGradient && hasTextGradientClass ? backgroundImage : rgbToHex(elStyle.color);\n        this.selectedColors.backgroundColor =\n            hasGradient && !hasTextGradientClass ? backgroundImage : rgbToHex(backgroundColor);\n    }\n\n    /**\n     * Apply a css or class color on the current selection (wrapped in <font>).\n     *\n     * @param {Object} param\n     * @param {string} param.color hexadecimal or bg-name/text-name class\n     * @param {string} param.mode 'color' or 'backgroundColor'\n     */\n    applyColor({ color, mode }) {\n        this.previewableApplyColor.commit(color, mode);\n        this.updateSelectedColor();\n    }\n    /**\n     * Apply a css or class color on the current selection (wrapped in <font>)\n     * in preview mode so that it can be reset.\n     *\n     * @param {Object} param\n     * @param {string} param.color hexadecimal or bg-name/text-name class\n     * @param {string} param.mode 'color' or 'backgroundColor'\n     */\n    applyColorPreview({ color, mode }) {\n        // Preview the color before applying it.\n        this.previewableApplyColor.preview(color, mode, true);\n        this.updateSelectedColor();\n    }\n    /**\n     * Reset the color applied in preview mode.\n     */\n    applyColorResetPreview() {\n        this.previewableApplyColor.revert();\n        this.updateSelectedColor();\n    }\n\n    removeAllColor() {\n        const colorModes = [\"color\", \"backgroundColor\"];\n        let someColorWasRemoved = true;\n        while (someColorWasRemoved) {\n            someColorWasRemoved = false;\n            for (const mode of colorModes) {\n                let max = 40;\n                const hasAnySelectedNodeColor = (mode) => {\n                    const nodes = this.dependencies.selection\n                        .getTraversedNodes()\n                        .filter((n) => isTextNode(n) || n.classList.contains(\"o_selected_td\"));\n                    return hasAnyNodesColor(nodes, mode);\n                };\n                while (hasAnySelectedNodeColor(mode) && max > 0) {\n                    this._applyColor(\"\", mode);\n                    someColorWasRemoved = true;\n                    max--;\n                }\n                if (max === 0) {\n                    someColorWasRemoved = false;\n                    throw new Error(\"Infinite Loop in removeAllColor().\");\n                }\n            }\n        }\n    }\n\n    /**\n     * Apply a css or class color on the current selection (wrapped in <font>).\n     *\n     * @param {string} color hexadecimal or bg-name/text-name class\n     * @param {string} mode 'color' or 'backgroundColor'\n     * @param {boolean} [previewMode=false] true - apply color in preview mode\n     */\n    _applyColor(color, mode, previewMode = false) {\n        if (this.delegateTo(\"color_apply_overrides\", color, mode, previewMode)) {\n            return;\n        }\n        const activeTab = document\n            .querySelector(\".o_font_color_selector button.active\")\n            ?.innerHTML.trim();\n        if (mode === \"backgroundColor\" && activeTab === \"Solid\" && color.startsWith(\"#\")) {\n            // Apply default transparency to selected solid tab colors in background\n            // mode to make text highlighting more usable between light and dark modes.\n            color += HEX_OPACITY;\n        }\n        let selection = this.dependencies.selection.getEditableSelection();\n        let selectionNodes;\n        // Get the <font> nodes to color\n        if (selection.isCollapsed) {\n            let zws;\n            if (\n                selection.anchorNode.nodeType !== Node.TEXT_NODE &&\n                selection.anchorNode.textContent !== \"\\u200b\"\n            ) {\n                zws = selection.anchorNode;\n            } else {\n                zws = this.dependencies.format.insertAndSelectZws();\n            }\n            selection = this.dependencies.selection.setSelection(\n                {\n                    anchorNode: zws,\n                    anchorOffset: 0,\n                },\n                { normalize: false }\n            );\n            selectionNodes = [zws];\n        } else {\n            selection = this.dependencies.split.splitSelection();\n            selectionNodes = this.dependencies.selection\n                .getSelectedNodes()\n                .filter((node) => isContentEditable(node) && node.nodeName !== \"T\");\n            if (isEmptyBlock(selection.endContainer)) {\n                selectionNodes.push(selection.endContainer, ...descendants(selection.endContainer));\n            }\n        }\n\n        const selectedNodes =\n            mode === \"backgroundColor\" && color\n                ? selectionNodes.filter((node) => !closestElement(node, \"table.o_selected_table\"))\n                : selectionNodes;\n\n        const selectedFieldNodes = new Set(\n            this.dependencies.selection\n                .getSelectedNodes()\n                .map((n) => closestElement(n, \"*[t-field],*[t-out],*[t-esc]\"))\n                .filter(Boolean)\n        );\n\n        const getFonts = (selectedNodes) => {\n            return selectedNodes.flatMap((node) => {\n                let font = closestElement(node, \"font\") || closestElement(node, \"span\");\n                const children = font && descendants(font);\n                if (\n                    font &&\n                    (font.nodeName === \"FONT\" || (font.nodeName === \"SPAN\" && font.style[mode]))\n                ) {\n                    // Partially selected <font>: split it.\n                    const selectedChildren = children.filter((child) =>\n                        selectedNodes.includes(child)\n                    );\n                    if (selectedChildren.length) {\n                        font = this.dependencies.split.splitAroundUntil(selectedChildren, font);\n                    } else {\n                        font = [];\n                    }\n                } else if (\n                    (node.nodeType === Node.TEXT_NODE && !isWhitespace(node) && !isZwnbsp(node)) ||\n                    (node.nodeName === \"BR\" && isEmptyBlock(node.parentNode)) ||\n                    (node.nodeType === Node.ELEMENT_NODE &&\n                        [\"inline\", \"inline-block\"].includes(getComputedStyle(node).display) &&\n                        !isWhitespace(node.textContent) &&\n                        !node.classList.contains(\"btn\") &&\n                        !node.querySelector(\"font\") &&\n                        node.nodeName !== \"A\" &&\n                        !(node.nodeName === \"SPAN\" && node.style[\"fontSize\"]))\n                ) {\n                    // Node is a visible text or inline node without font nor a button:\n                    // wrap it in a <font>.\n                    const previous = node.previousSibling;\n                    const classRegex = mode === \"color\" ? BG_CLASSES_REGEX : TEXT_CLASSES_REGEX;\n                    if (\n                        previous &&\n                        previous.nodeName === \"FONT\" &&\n                        !previous.style[mode === \"color\" ? \"backgroundColor\" : \"color\"] &&\n                        !classRegex.test(previous.className) &&\n                        selectedNodes.includes(previous.firstChild) &&\n                        selectedNodes.includes(previous.lastChild)\n                    ) {\n                        // Directly follows a fully selected <font> that isn't\n                        // colored in the other mode: append to that.\n                        font = previous;\n                    } else {\n                        // No <font> found: insert a new one.\n                        font = this.document.createElement(\"font\");\n                        node.after(font);\n                    }\n                    if (node.textContent) {\n                        font.appendChild(node);\n                    } else {\n                        fillEmpty(font);\n                    }\n                } else {\n                    font = []; // Ignore non-text or invisible text nodes.\n                }\n                return font;\n            });\n        };\n\n        for (const fieldNode of selectedFieldNodes) {\n            this.colorElement(fieldNode, color, mode);\n        }\n\n        let fonts = getFonts(selectedNodes);\n        // Dirty fix as the previous call could have unconnected elements\n        // because of the `splitAroundUntil`. Another call should provide he\n        // correct list of fonts.\n        if (!fonts.every((font) => font.isConnected)) {\n            fonts = getFonts(selectedNodes);\n        }\n\n        // Color the selected <font>s and remove uncolored fonts.\n        const fontsSet = new Set(fonts);\n        for (const font of fontsSet) {\n            this.colorElement(font, color, mode);\n            if (\n                !hasColor(font, \"color\") &&\n                !hasColor(font, \"backgroundColor\") &&\n                (!font.hasAttribute(\"style\") || !color)\n            ) {\n                for (const child of [...font.childNodes]) {\n                    font.parentNode.insertBefore(child, font);\n                }\n                font.parentNode.removeChild(font);\n                fontsSet.delete(font);\n            }\n        }\n        this.dependencies.selection.setSelection(selection, { normalize: false });\n    }\n\n    getUsedCustomColors(mode) {\n        const allFont = this.editable.querySelectorAll(\"font\");\n        const usedCustomColors = new Set();\n        for (const font of allFont) {\n            if (isCSSColor(font.style[mode])) {\n                usedCustomColors.add(rgbToHex(font.style[mode]));\n            }\n        }\n        return usedCustomColors;\n    }\n\n    /**\n     * Applies a css or class color (fore- or background-) to an element.\n     * Replace the color that was already there if any.\n     *\n     * @param {Element} element\n     * @param {string} color hexadecimal or bg-name/text-name class\n     * @param {'color'|'backgroundColor'} mode 'color' or 'backgroundColor'\n     */\n    colorElement(element, color, mode) {\n        const newClassName = element.className\n            .replace(mode === \"color\" ? TEXT_CLASSES_REGEX : BG_CLASSES_REGEX, \"\")\n            .replace(/\\btext-gradient\\b/g, \"\") // cannot be combined with setting a background\n            .replace(/\\s+/, \" \");\n        element.className !== newClassName && (element.className = newClassName);\n        element.style[\"background-image\"] = \"\";\n        if (mode === \"backgroundColor\") {\n            element.style[\"background\"] = \"\";\n        }\n        if (color.startsWith(\"text\") || color.startsWith(\"bg-\")) {\n            element.style[mode] = \"\";\n            element.classList.add(color);\n        } else if (isColorGradient(color)) {\n            element.style[mode] = \"\";\n            if (mode === \"color\") {\n                element.style[\"background\"] = \"\";\n                element.style[\"background-image\"] = color;\n                element.classList.add(\"text-gradient\");\n            } else {\n                element.style[\"background-image\"] = color;\n            }\n        } else {\n            element.style[mode] = color;\n        }\n    }\n}\n", "import { Component, useRef, useState } from \"@odoo/owl\";\nimport { Colorpicker } from \"@web/core/colorpicker/colorpicker\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { isCSSColor } from \"@web/core/utils/colors\";\nimport { isColorGradient } from \"@html_editor/utils/color\";\nimport { GradientPicker } from \"./gradient_picker\";\nimport { toolbarButtonProps } from \"@html_editor/main/toolbar/toolbar\";\n\n// These colors are already normalized as per normalizeCSSColor in @web/legacy/js/widgets/colorpicker\nconst DEFAULT_COLORS = [\n    [\"#000000\", \"#424242\", \"#636363\", \"#9C9C94\", \"#CEC6CE\", \"#EFEFEF\", \"#F7F7F7\", \"#FFFFFF\"],\n    [\"#FF0000\", \"#FF9C00\", \"#FFFF00\", \"#00FF00\", \"#00FFFF\", \"#0000FF\", \"#9C00FF\", \"#FF00FF\"],\n    [\"#F7C6CE\", \"#FFE7CE\", \"#FFEFC6\", \"#D6EFD6\", \"#CEDEE7\", \"#CEE7F7\", \"#D6D6E7\", \"#E7D6DE\"],\n    [\"#E79C9C\", \"#FFC69C\", \"#FFE79C\", \"#B5D6A5\", \"#A5C6CE\", \"#9CC6EF\", \"#B5A5D6\", \"#D6A5BD\"],\n    [\"#E76363\", \"#F7AD6B\", \"#FFD663\", \"#94BD7B\", \"#73A5AD\", \"#6BADDE\", \"#8C7BC6\", \"#C67BA5\"],\n    [\"#CE0000\", \"#E79439\", \"#EFC631\", \"#6BA54A\", \"#4A7B8C\", \"#3984C6\", \"#634AA5\", \"#A54A7B\"],\n    [\"#9C0000\", \"#B56308\", \"#BD9400\", \"#397B21\", \"#104A5A\", \"#085294\", \"#311873\", \"#731842\"],\n    [\"#630000\", \"#7B3900\", \"#846300\", \"#295218\", \"#083139\", \"#003163\", \"#21104A\", \"#4A1031\"],\n];\n\nconst DEFAULT_GRADIENT_COLORS = [\n    \"linear-gradient(135deg, rgb(255, 204, 51) 0%, rgb(226, 51, 255) 100%)\",\n    \"linear-gradient(135deg, rgb(102, 153, 255) 0%, rgb(255, 51, 102) 100%)\",\n    \"linear-gradient(135deg, rgb(47, 128, 237) 0%, rgb(178, 255, 218) 100%)\",\n    \"linear-gradient(135deg, rgb(203, 94, 238) 0%, rgb(75, 225, 236) 100%)\",\n    \"linear-gradient(135deg, rgb(214, 255, 127) 0%, rgb(0, 179, 204) 100%)\",\n    \"linear-gradient(135deg, rgb(255, 222, 69) 0%, rgb(69, 33, 0) 100%)\",\n    \"linear-gradient(135deg, rgb(222, 222, 222) 0%, rgb(69, 69, 69) 100%)\",\n    \"linear-gradient(135deg, rgb(255, 222, 202) 0%, rgb(202, 115, 69) 100%)\",\n];\n\nexport class ColorSelector extends Component {\n    static template = \"html_editor.ColorSelector\";\n    static components = { Dropdown, Colorpicker, GradientPicker };\n    static props = {\n        type: String, // either foreground or background\n        getUsedCustomColors: Function,\n        getSelectedColors: Function,\n        applyColor: Function,\n        applyColorPreview: Function,\n        applyColorResetPreview: Function,\n        focusEditable: Function,\n        ...toolbarButtonProps,\n    };\n\n    setup() {\n        this.DEFAULT_COLORS = DEFAULT_COLORS;\n        this.DEFAULT_GRADIENT_COLORS = DEFAULT_GRADIENT_COLORS;\n        this.dropdown = useDropdownState({\n            onClose: () => this.props.applyColorResetPreview(),\n        });\n\n        this.mode = this.props.type === \"foreground\" ? \"color\" : \"backgroundColor\";\n\n        this.state = useState({ activeTab: \"solid\" });\n        this.colorWrapperEl = useRef(\"colorsWrapper\");\n        this.selectedColors = useState(this.props.getSelectedColors());\n        this.defaultColor = this.selectedColors[this.mode];\n        this.currentCustomColor = useState({ color: this.selectedColors[this.mode] });\n\n        this.usedCustomColors = this.props.getUsedCustomColors();\n    }\n\n    setTab(tab) {\n        this.state.activeTab = tab;\n    }\n\n    processColorFromEvent(ev) {\n        let color = ev.target.dataset.color;\n        if (color && !isCSSColor(color) && !isColorGradient(color)) {\n            color = (this.mode === \"color\" ? \"text-\" : \"bg-\") + color;\n        }\n        return color;\n    }\n\n    applyColor(color) {\n        this.currentCustomColor.color = color;\n        this.props.applyColor({ color: color || \"\", mode: this.mode });\n        this.props.focusEditable();\n    }\n\n    onColorApply(ev) {\n        if (ev.target.tagName !== \"BUTTON\") {\n            return;\n        }\n        const color = this.processColorFromEvent(ev);\n        this.applyColor(color);\n        this.dropdown.close();\n    }\n\n    onColorPreview(ev) {\n        const color = ev.hex ? ev.hex : this.processColorFromEvent(ev);\n        this.props.applyColorPreview({ color: color || \"\", mode: this.mode });\n    }\n\n    onColorHover(ev) {\n        if (ev.target.tagName !== \"BUTTON\") {\n            return;\n        }\n        this.onColorPreview(ev);\n    }\n\n    onColorHoverOut(ev) {\n        if (ev.target.tagName !== \"BUTTON\") {\n            return;\n        }\n        this.props.applyColorResetPreview();\n    }\n\n    getCurrentGradientColor() {\n        if (isColorGradient(this.selectedColors[this.mode])) {\n            return this.selectedColors[this.mode];\n        }\n    }\n\n    getSelectedColorStyle() {\n        if (isColorGradient(this.selectedColors[this.mode])) {\n            return `border-bottom: 2px solid transparent; border-image: ${\n                this.selectedColors[this.mode]\n            }; border-image-slice: 1`;\n        }\n        return `border-bottom: 2px solid ${this.selectedColors[this.mode]}`;\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { isBlock, closestBlock } from \"@html_editor/utils/blocks\";\nimport { fillEmpty } from \"@html_editor/utils/dom\";\nimport { leftLeafOnlyNotBlockPath } from \"@html_editor/utils/dom_state\";\nimport { isVisibleTextNode } from \"@html_editor/utils/dom_info\";\nimport {\n    closestElement,\n    createDOMPathGenerator,\n    descendants,\n} from \"@html_editor/utils/dom_traversal\";\nimport {\n    convertNumericToUnit,\n    getCSSVariableValue,\n    getHtmlStyle,\n    getFontSizeDisplayValue,\n} from \"@html_editor/utils/formatting\";\nimport { DIRECTIONS } from \"@html_editor/utils/position\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { FontSelector } from \"./font_selector\";\nimport { getBaseContainerSelector } from \"@html_editor/utils/base_container\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { reactive } from \"@odoo/owl\";\nimport { FontSizeSelector } from \"./font_size_selector\";\n\nexport const fontItems = [\n    {\n        name: _t(\"Header 1 Display 1\"),\n        tagName: \"h1\",\n        extraClass: \"display-1\",\n    },\n    // TODO @phoenix use them if showExtendedTextStylesOptions is true\n    // {\n    //     name: _t(\"Header 1 Display 2\"),\n    //     tagName: \"h1\",\n    //     extraClass: \"display-2\",\n    // },\n    // {\n    //     name: _t(\"Header 1 Display 3\"),\n    //     tagName: \"h1\",\n    //     extraClass: \"display-3\",\n    // },\n    // {\n    //     name: _t(\"Header 1 Display 4\"),\n    //     tagName: \"h1\",\n    //     extraClass: \"display-4\",\n    // },\n    // ----\n\n    { name: _t(\"Header 1\"), tagName: \"h1\" },\n    { name: _t(\"Header 2\"), tagName: \"h2\" },\n    { name: _t(\"Header 3\"), tagName: \"h3\" },\n    { name: _t(\"Header 4\"), tagName: \"h4\" },\n    { name: _t(\"Header 5\"), tagName: \"h5\" },\n    { name: _t(\"Header 6\"), tagName: \"h6\" },\n\n    {\n        name: _t(\"Normal\"),\n        tagName: \"div\",\n        // for the FontSelector component\n        selector: getBaseContainerSelector(\"DIV\"),\n    },\n    { name: _t(\"Paragraph\"), tagName: \"p\" },\n\n    // TODO @phoenix use them if showExtendedTextStylesOptions is true\n    // consider baseContainer if enabling them\n    // {\n    //     name: _t(\"Light\"),\n    //     tagName: \"p\",\n    //     extraClass: \"lead\",\n    // },\n    // {\n    //     name: _t(\"Small\"),\n    //     tagName: \"p\",\n    //     extraClass: \"small\",\n    // },\n    // ----\n\n    { name: _t(\"Code\"), tagName: \"pre\" },\n    { name: _t(\"Quote\"), tagName: \"blockquote\" },\n];\n\nexport const fontSizeItems = [\n    {\n        variableName: \"display-1-font-size\",\n        className: \"display-1-fs\",\n    },\n    { variableName: \"display-2-font-size\", className: \"display-2-fs\" },\n    { variableName: \"display-3-font-size\", className: \"display-3-fs\" },\n    { variableName: \"display-4-font-size\", className: \"display-4-fs\" },\n    { variableName: \"h1-font-size\", className: \"h1-fs\" },\n    { variableName: \"h2-font-size\", className: \"h2-fs\" },\n    { variableName: \"h3-font-size\", className: \"h3-fs\" },\n    { variableName: \"h4-font-size\", className: \"h4-fs\" },\n    { variableName: \"h5-font-size\", className: \"h5-fs\" },\n    { variableName: \"h6-font-size\", className: \"h6-fs\" },\n    { variableName: \"font-size-base\", className: \"base-fs\" },\n    { variableName: \"small-font-size\", className: \"o_small-fs\" },\n];\n\nconst rightLeafOnlyNotBlockPath = createDOMPathGenerator(DIRECTIONS.RIGHT, {\n    leafOnly: true,\n    stopTraverseFunction: isBlock,\n    stopFunction: isBlock,\n});\n\nconst headingTags = [\"H1\", \"H2\", \"H3\", \"H4\", \"H5\", \"H6\"];\nconst handledElemSelector = [...headingTags, \"PRE\", \"BLOCKQUOTE\"].join(\", \");\n\nexport class FontPlugin extends Plugin {\n    static id = \"font\";\n    static dependencies = [\"baseContainer\", \"input\", \"split\", \"selection\", \"dom\", \"format\"];\n    resources = {\n        user_commands: [\n            {\n                id: \"setTagHeading1\",\n                title: _t(\"Heading 1\"),\n                description: _t(\"Big section heading\"),\n                icon: \"fa-header\",\n                run: () => this.dependencies.dom.setTag({ tagName: \"H1\" }),\n            },\n            {\n                id: \"setTagHeading2\",\n                title: _t(\"Heading 2\"),\n                description: _t(\"Medium section heading\"),\n                icon: \"fa-header\",\n                run: () => this.dependencies.dom.setTag({ tagName: \"H2\" }),\n            },\n            {\n                id: \"setTagHeading3\",\n                title: _t(\"Heading 3\"),\n                description: _t(\"Small section heading\"),\n                icon: \"fa-header\",\n                run: () => this.dependencies.dom.setTag({ tagName: \"H3\" }),\n            },\n            {\n                id: \"setTagParagraph\",\n                title: _t(\"Text\"),\n                description: _t(\"Paragraph block\"),\n                icon: \"fa-paragraph\",\n                run: () => {\n                    this.dependencies.dom.setTag({\n                        tagName: this.dependencies.baseContainer.getDefaultNodeName(),\n                    });\n                },\n            },\n            {\n                id: \"setTagQuote\",\n                title: _t(\"Quote\"),\n                description: _t(\"Add a blockquote section\"),\n                icon: \"fa-quote-right\",\n                run: () => this.dependencies.dom.setTag({ tagName: \"blockquote\" }),\n            },\n            {\n                id: \"setTagPre\",\n                title: _t(\"Code\"),\n                description: _t(\"Add a code section\"),\n                icon: \"fa-code\",\n                run: () => this.dependencies.dom.setTag({ tagName: \"pre\" }),\n            },\n        ],\n        toolbar_groups: [\n            withSequence(10, {\n                id: \"font\",\n            }),\n            withSequence(29, {\n                id: \"font-size\",\n            }),\n        ],\n        toolbar_items: [\n            {\n                id: \"font\",\n                groupId: \"font\",\n                title: _t(\"Font style\"),\n                Component: FontSelector,\n                props: {\n                    getItems: () => fontItems,\n                    getDisplay: () => this.font,\n                    onSelected: (item) => {\n                        this.dependencies.dom.setTag({\n                            tagName: item.tagName,\n                            extraClass: item.extraClass,\n                        });\n                        this.updateFontSelectorParams();\n                    },\n                },\n            },\n            {\n                id: \"font-size\",\n                groupId: \"font-size\",\n                title: _t(\"Font size\"),\n                Component: FontSizeSelector,\n                props: {\n                    getItems: () => this.fontSizeItems,\n                    getDisplay: () => this.fontSize,\n                    onFontSizeInput: (size) => {\n                        this.dependencies.format.formatSelection(\"fontSize\", {\n                            formatProps: { size },\n                            applyStyle: true,\n                        });\n                        this.updateFontSizeSelectorParams();\n                    },\n                    onSelected: (item) => {\n                        this.dependencies.format.formatSelection(\"setFontSizeClassName\", {\n                            formatProps: { className: item.className },\n                            applyStyle: true,\n                        });\n                        this.updateFontSizeSelectorParams();\n                    },\n                },\n            },\n        ],\n        powerbox_categories: withSequence(30, { id: \"format\", name: _t(\"Format\") }),\n        powerbox_items: [\n            {\n                categoryId: \"format\",\n                commandId: \"setTagHeading1\",\n            },\n            {\n                categoryId: \"format\",\n                commandId: \"setTagHeading2\",\n            },\n            {\n                categoryId: \"format\",\n                commandId: \"setTagHeading3\",\n            },\n            {\n                categoryId: \"format\",\n                commandId: \"setTagParagraph\",\n            },\n            {\n                categoryId: \"structure\",\n                commandId: \"setTagQuote\",\n            },\n            {\n                categoryId: \"structure\",\n                commandId: \"setTagPre\",\n            },\n        ],\n        hints: [\n            { selector: \"H1\", text: _t(\"Heading 1\") },\n            { selector: \"H2\", text: _t(\"Heading 2\") },\n            { selector: \"H3\", text: _t(\"Heading 3\") },\n            { selector: \"H4\", text: _t(\"Heading 4\") },\n            { selector: \"H5\", text: _t(\"Heading 5\") },\n            { selector: \"H6\", text: _t(\"Heading 6\") },\n            { selector: \"PRE\", text: _t(\"Code\") },\n            { selector: \"BLOCKQUOTE\", text: _t(\"Quote\") },\n        ],\n\n        /** Handlers */\n        input_handlers: this.onInput.bind(this),\n        selectionchange_handlers: [\n            this.updateFontSelectorParams.bind(this),\n            this.updateFontSizeSelectorParams.bind(this),\n        ],\n        post_undo_handlers: [\n            this.updateFontSelectorParams.bind(this),\n            this.updateFontSizeSelectorParams.bind(this),\n        ],\n        post_redo_handlers: [\n            this.updateFontSelectorParams.bind(this),\n            this.updateFontSizeSelectorParams.bind(this),\n        ],\n\n        /** Overrides */\n        split_element_block_overrides: [\n            this.handleSplitBlockHeading.bind(this),\n            this.handleSplitBlockPRE.bind(this),\n            this.handleSplitBlockquote.bind(this),\n        ],\n        delete_backward_overrides: withSequence(20, this.handleDeleteBackward.bind(this)),\n        delete_backward_word_overrides: this.handleDeleteBackward.bind(this),\n    };\n\n    setup() {\n        this.fontSize = reactive({ displayName: \"\" });\n        this.font = reactive({ displayName: \"\" });\n    }\n\n    get fontName() {\n        const sel = this.dependencies.selection.getSelectionData().deepEditableSelection;\n        // if (!sel) {\n        //     return \"Normal\";\n        // }\n        const anchorNode = sel.anchorNode;\n        const block = closestBlock(anchorNode);\n        const tagName = block.tagName.toLowerCase();\n\n        const matchingItems = fontItems.filter((item) => {\n            return item.selector ? block.matches(item.selector) : item.tagName === tagName;\n        });\n\n        const matchingItemsWitoutExtraClass = matchingItems.filter((item) => !item.extraClass);\n\n        if (!matchingItems.length) {\n            return _t(\"Normal\");\n        }\n\n        return (\n            matchingItems.find((item) => block.classList.contains(item.extraClass)) ||\n            (matchingItemsWitoutExtraClass.length && matchingItemsWitoutExtraClass[0])\n        ).name;\n    }\n\n    get fontSizeName() {\n        const sel = this.dependencies.selection.getSelectionData().deepEditableSelection;\n        if (!sel) {\n            return fontSizeItems[0].name;\n        }\n        return Math.round(getFontSizeDisplayValue(sel, this.document));\n    }\n\n    get fontSizeItems() {\n        const style = getHtmlStyle(this.document);\n        const nameAlreadyUsed = new Set();\n        return fontSizeItems.flatMap((item) => {\n            const strValue = getCSSVariableValue(item.variableName, style);\n            if (!strValue) {\n                return [];\n            }\n            const remValue = parseFloat(strValue);\n            const pxValue = convertNumericToUnit(remValue, \"rem\", \"px\", style);\n            const roundedValue = Math.round(pxValue);\n            if (nameAlreadyUsed.has(roundedValue)) {\n                return [];\n            }\n            nameAlreadyUsed.add(roundedValue);\n\n            return [{ ...item, tagName: \"span\", name: roundedValue }];\n        });\n    }\n\n    // @todo @phoenix: Move this to a specific Pre/CodeBlock plugin?\n    /**\n     * Specific behavior for pre: insert newline (\\n) in text or insert p at\n     * end.\n     */\n    handleSplitBlockPRE({ targetNode, targetOffset }) {\n        const closestPre = closestElement(targetNode, \"pre\");\n        const closestBlockNode = closestBlock(targetNode);\n        if (\n            !closestPre ||\n            (closestBlockNode.nodeName !== \"PRE\" &&\n                (closestBlockNode.textContent || closestBlockNode.nextSibling))\n        ) {\n            return;\n        }\n\n        // Nodes to the right of the split position.\n        const nodesAfterTarget = [...rightLeafOnlyNotBlockPath(targetNode, targetOffset)];\n        if (\n            !nodesAfterTarget.length ||\n            (nodesAfterTarget.length === 1 && nodesAfterTarget[0].nodeName === \"BR\")\n        ) {\n            // Remove the last empty block node within pre tag\n            if (closestBlockNode.nodeName !== \"PRE\") {\n                closestBlockNode.remove();\n            }\n            const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n            const dir = closestBlockNode.getAttribute(\"dir\") || closestPre.getAttribute(\"dir\");\n            if (dir) {\n                baseContainer.setAttribute(\"dir\", dir);\n            }\n            closestPre.after(baseContainer);\n            fillEmpty(baseContainer);\n            this.dependencies.selection.setCursorStart(baseContainer);\n        } else {\n            const lineBreak = this.document.createElement(\"br\");\n            targetNode.insertBefore(lineBreak, targetNode.childNodes[targetOffset]);\n            this.dependencies.selection.setCursorEnd(lineBreak);\n        }\n        return true;\n    }\n\n    /**\n     * Specific behavior for blockquote: insert p at end and remove the last\n     * empty node.\n     */\n    handleSplitBlockquote({ targetNode, targetOffset }) {\n        const closestQuote = closestElement(targetNode, \"blockquote\");\n        const closestBlockNode = closestBlock(targetNode);\n        if (\n            !closestQuote ||\n            (closestBlockNode.nodeName !== \"BLOCKQUOTE\" &&\n                (closestBlockNode.textContent || closestBlockNode.nextSibling))\n        ) {\n            return;\n        }\n\n        // Nodes to the right of the split position.\n        const nodesAfterTarget = [...rightLeafOnlyNotBlockPath(targetNode, targetOffset)];\n        if (\n            !nodesAfterTarget.length ||\n            (nodesAfterTarget.length === 1 && nodesAfterTarget[0].nodeName === \"BR\")\n        ) {\n            // Remove the last empty block node within blockquote tag\n            if (closestBlockNode.nodeName !== \"BLOCKQUOTE\") {\n                closestBlockNode.remove();\n            }\n            const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n            const dir = closestBlockNode.getAttribute(\"dir\") || closestQuote.getAttribute(\"dir\");\n            if (dir) {\n                baseContainer.setAttribute(\"dir\", dir);\n            }\n            closestQuote.after(baseContainer);\n            fillEmpty(baseContainer);\n            this.dependencies.selection.setCursorStart(baseContainer);\n            return true;\n        }\n    }\n\n    // @todo @phoenix: Move this to a specific Heading plugin?\n    /**\n     * Specific behavior for headings: do not split in two if cursor at the end but\n     * instead create a paragraph.\n     * Cursor end of line: <h1>title[]</h1> + ENTER <=> <h1>title</h1><p>[]<br/></p>\n     * Cursor in the line: <h1>tit[]le</h1> + ENTER <=> <h1>tit</h1><h1>[]le</h1>\n     */\n    handleSplitBlockHeading(params) {\n        const closestHeading = closestElement(params.targetNode, (element) =>\n            headingTags.includes(element.tagName)\n        );\n        if (closestHeading) {\n            const [, newElement] = this.dependencies.split.splitElementBlock(params);\n            // @todo @phoenix: if this condition can be anticipated before the split,\n            // handle the splitBlock only in such case.\n            if (\n                newElement &&\n                headingTags.includes(newElement.tagName) &&\n                !descendants(newElement).some(isVisibleTextNode)\n            ) {\n                const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n                const dir = newElement.getAttribute(\"dir\");\n                if (dir) {\n                    baseContainer.setAttribute(\"dir\", dir);\n                }\n                newElement.replaceWith(baseContainer);\n                baseContainer.replaceChildren(this.document.createElement(\"br\"));\n                this.dependencies.selection.setCursorStart(baseContainer);\n            }\n            return true;\n        }\n    }\n\n    /**\n     * Transform an empty heading, blockquote or pre at the beginning of the\n     * editable into a paragraph.\n     */\n    handleDeleteBackward({ startContainer, startOffset, endContainer, endOffset }) {\n        // Detect if cursor is at the start of the editable (collapsed range).\n        const rangeIsCollapsed = startContainer === endContainer && startOffset === endOffset;\n        if (!rangeIsCollapsed) {\n            return;\n        }\n        // Check if cursor is inside an empty heading, blockquote or pre.\n        const closestHandledElement = closestElement(endContainer, handledElemSelector);\n        if (!closestHandledElement || closestHandledElement.textContent.length) {\n            return;\n        }\n        // Check if unremovable.\n        if (this.getResource(\"unremovable_node_predicates\").some((p) => p(closestHandledElement))) {\n            return;\n        }\n        const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n        baseContainer.append(...closestHandledElement.childNodes);\n        closestHandledElement.after(baseContainer);\n        closestHandledElement.remove();\n        this.dependencies.selection.setCursorStart(baseContainer);\n        return true;\n    }\n\n    onInput(ev) {\n        if (ev.data !== \" \") {\n            return;\n        }\n        const selection = this.dependencies.selection.getEditableSelection();\n        const blockEl = closestBlock(selection.anchorNode);\n        const leftDOMPath = leftLeafOnlyNotBlockPath(selection.anchorNode);\n        let spaceOffset = selection.anchorOffset;\n        let leftLeaf = leftDOMPath.next().value;\n        while (leftLeaf) {\n            // Calculate spaceOffset by adding lengths of previous text nodes\n            // to correctly find offset position for selection within inline\n            // elements. e.g. <p>ab<strong>cd []e</strong></p>\n            spaceOffset += leftLeaf.length;\n            leftLeaf = leftDOMPath.next().value;\n        }\n        const precedingText = blockEl.textContent.substring(0, spaceOffset);\n        if (/^(#{1,6})\\s$/.test(precedingText)) {\n            const numberOfHash = precedingText.length - 1;\n            const headingToBe = headingTags[numberOfHash - 1];\n            this.dependencies.selection.setSelection({\n                anchorNode: blockEl.firstChild,\n                anchorOffset: 0,\n                focusNode: selection.focusNode,\n                focusOffset: selection.focusOffset,\n            });\n            this.dependencies.selection.extractContent(\n                this.dependencies.selection.getEditableSelection()\n            );\n            fillEmpty(blockEl);\n            this.dependencies.dom.setTag({ tagName: headingToBe });\n        }\n    }\n\n    updateFontSelectorParams() {\n        this.font.displayName = this.fontName;\n    }\n\n    updateFontSizeSelectorParams() {\n        this.fontSize.displayName = this.fontSizeName;\n    }\n}\n", "import { Component, useState } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { toolbarButtonProps } from \"@html_editor/main/toolbar/toolbar\";\n\nexport class FontSelector extends Component {\n    static template = \"html_editor.FontSelector\";\n    static props = {\n        getItems: Function,\n        getDisplay: Function,\n        onSelected: Function,\n        ...toolbarButtonProps,\n    };\n    static components = { Dropdown, DropdownItem };\n\n    setup() {\n        this.items = this.props.getItems();\n        this.state = useState(this.props.getDisplay());\n    }\n\n    onSelected(item) {\n        this.props.onSelected(item);\n    }\n}\n", "import { Component, onMounted, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { toolbarButtonProps } from \"@html_editor/main/toolbar/toolbar\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { useDebounced } from \"@web/core/utils/timing\";\n\nconst MAX_FONT_SIZE = 144;\n\nexport class FontSizeSelector extends Component {\n    static template = \"html_editor.FontSizeSelector\";\n    static props = {\n        getItems: Function,\n        getDisplay: Function,\n        onFontSizeInput: Function,\n        onSelected: Function,\n        ...toolbarButtonProps,\n    };\n    static components = { Dropdown, DropdownItem };\n\n    setup() {\n        this.items = this.props.getItems();\n        this.state = useState(this.props.getDisplay());\n        this.dropdown = useDropdownState();\n        this.iframeContentRef = useRef(\"iframeContent\");\n        this.debouncedCustomFontSizeInput = useDebounced(this.onCustomFontSizeInput, 200);\n\n        onMounted(() => {\n            const iframeEl = this.iframeContentRef.el;\n            const iframeDoc = iframeEl.contentWindow.document;\n            this.fontSizeInput = iframeDoc.createElement(\"input\");\n            Object.assign(iframeDoc.body.style, {\n                padding: \"0\",\n                margin: \"0\",\n            });\n            Object.assign(this.fontSizeInput.style, {\n                width: \"100%\",\n                height: \"100%\",\n                border: \"none\",\n                outline: \"none\",\n                textAlign: \"center\",\n            });\n            this.fontSizeInput.type = \"text\";\n            this.fontSizeInput.name = \"font-size-input\";\n            this.fontSizeInput.autocomplete = \"off\";\n            iframeDoc.body.appendChild(this.fontSizeInput);\n            this.fontSizeInput.addEventListener(\"click\", () => {\n                if (!this.dropdown.isOpen) {\n                    this.dropdown.open();\n                }\n            });\n            this.fontSizeInput.addEventListener(\"input\", this.debouncedCustomFontSizeInput);\n            this.fontSizeInput.addEventListener(\"keydown\", this.onKeyDownFontSizeInput.bind(this));\n        });\n        useEffect(\n            () => {\n                // Update `fontSizeInputValue` whenever the font size changes.\n                this.fontSizeInput.value = this.state.displayName;\n            },\n            () => [this.state.displayName]\n        );\n        useEffect(\n            () => {\n                // Focus input on dropdown open, blur on close.\n                this.dropdown.isOpen ? this.fontSizeInput.select() : this.fontSizeInput.blur();\n            },\n            () => [this.dropdown.isOpen]\n        );\n    }\n\n    onCustomFontSizeInput(ev) {\n        let fontSize = parseInt(ev.target.value, 10);\n        if (fontSize > 0) {\n            fontSize = Math.min(fontSize, MAX_FONT_SIZE);\n            if (this.state.displayName !== fontSize) {\n                this.props.onFontSizeInput(`${fontSize}px`);\n            } else {\n                // Reset input if state.displayName does not change.\n                this.fontSizeInput.value = this.state.displayName;\n            }\n        }\n    }\n\n    onKeyDownFontSizeInput(ev) {\n        if ([\"Enter\", \"Tab\"].includes(ev.key) && this.dropdown.isOpen) {\n            this.dropdown.close();\n        } else if ([\"ArrowUp\", \"ArrowDown\"].includes(ev.key)) {\n            const fontSizeSelectorMenu = document.querySelector(\".o_font_size_selector_menu\");\n            if (!fontSizeSelectorMenu) {\n                return;\n            }\n            ev.target.blur();\n            const fontSizeMenuItemToFocus =\n                ev.key === \"ArrowUp\"\n                    ? fontSizeSelectorMenu.lastElementChild\n                    : fontSizeSelectorMenu.firstElementChild;\n            if (fontSizeMenuItemToFocus) {\n                fontSizeMenuItemToFocus.focus();\n            }\n        }\n    }\n\n    onSelected(item) {\n        this.props.onSelected(item);\n    }\n}\n", "import { Component, onWillUpdateProps, useState } from \"@odoo/owl\";\nimport { Colorpicker } from \"@web/core/colorpicker/colorpicker\";\nimport { isColorGradient, rgbToHex } from \"@html_editor/utils/color\";\n\nexport class GradientPicker extends Component {\n    static components = { Colorpicker };\n    static template = \"html_editor.GradientPicker\";\n    static props = {\n        onGradientChange: { type: Function, optional: true },\n        selectedGradient: { type: String, optional: true },\n    };\n\n    setup() {\n        this.state = useState({\n            type: \"linear\",\n            angle: 135,\n            currentColorIndex: 0,\n            size: \"closest-side\",\n        });\n        this.positions = useState({ x: 25, y: 25 });\n        this.colors = useState([\n            { hex: \"#FF00FF\", percentage: 0 },\n            { hex: \"#00FFFF\", percentage: 100 },\n        ]);\n        this.setGradientFromString(this.props.selectedGradient);\n\n        onWillUpdateProps((newProps) => {\n            if (newProps.selectedGradient) {\n                this.setGradientFromString(newProps.selectedGradient);\n            }\n        });\n    }\n\n    setGradientFromString(gradient) {\n        if (!gradient || !isColorGradient(gradient)) {\n            return;\n        }\n        const colors = [\n            ...gradient.matchAll(\n                /(#[0-9a-f]{6}|rgba?\\(\\s*[0-9]+\\s*,\\s*[0-9]+\\s*,\\s*[0-9]+\\s*[,\\s*[0-9.]*]?\\s*\\)|[a-z]+)\\s*([[0-9]+%]?)/g\n            ),\n        ].filter((color) => rgbToHex(color[1]) !== \"#\");\n\n        if (colors.length !== 2) {\n            return;\n        }\n\n        this.colors[0] = { hex: rgbToHex(colors[0][1]), percentage: colors[0][2].replace(\"%\", \"\") };\n        this.colors[1] = { hex: rgbToHex(colors[1][1]), percentage: colors[1][2].replace(\"%\", \"\") };\n\n        const isLinear = gradient.startsWith(\"linear-gradient(\");\n        if (isLinear) {\n            const angle = gradient.match(/([0-9]+)deg/);\n            if (angle) {\n                this.state.angle = angle[1];\n            }\n        } else {\n            this.state.type = \"radial\";\n            const sizeMatch = gradient.match(/(closest|farthest)-(side|corner)/);\n            const size = sizeMatch ? sizeMatch[0] : \"farthest-corner\";\n            this.state.size = size;\n\n            const position = gradient.match(/ at ([0-9]+)% ([0-9]+)%/) || [\"\", \"50\", \"50\"];\n            this.positions.x = position[1];\n            this.positions.y = position[2];\n        }\n    }\n\n    selectType(type) {\n        this.state.type = type;\n        this.onColorGradientChange();\n    }\n\n    onAngleChange(ev) {\n        this.state.angle = ev.target.value;\n        this.onColorGradientChange();\n    }\n\n    onPositionChange(position, ev) {\n        this.positions[position] = ev.target.value;\n        this.onColorGradientChange();\n    }\n\n    onColorChange(color) {\n        this.colors[this.state.currentColorIndex].hex = color.hex;\n        this.onColorGradientChange();\n    }\n\n    onSizeChange(size) {\n        this.state.size = size;\n        this.onColorGradientChange();\n    }\n\n    onColorPercentageChange(colorIndex, ev) {\n        this.state.currentColorIndex = colorIndex;\n        this.colors[colorIndex].percentage = ev.target.value;\n        if (this.colors[0].percentage > this.colors[1].percentage) {\n            this.colors[1].percentage = this.colors[0].percentage;\n        }\n        this.onColorGradientChange();\n    }\n\n    onColorGradientChange() {\n        if (this.state.type === \"linear\") {\n            this.props.onGradientChange(\n                `linear-gradient(${this.state.angle}deg, ${this.colors[0].hex} ${this.colors[0].percentage}%, ${this.colors[1].hex} ${this.colors[1].percentage}%)`\n            );\n        } else {\n            this.props.onGradientChange(\n                `radial-gradient(circle ${this.state.size} at ${this.positions.x}% ${this.positions.y}%, ${this.colors[0].hex} ${this.colors[0].percentage}%, ${this.colors[1].hex} ${this.colors[1].percentage}%)`\n            );\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { isEmptyBlock, isProtected } from \"@html_editor/utils/dom_info\";\nimport { removeClass } from \"@html_editor/utils/dom\";\nimport { childNodes, selectElements } from \"@html_editor/utils/dom_traversal\";\nimport { closestBlock } from \"../utils/blocks\";\nimport { baseContainerGlobalSelector } from \"@html_editor/utils/base_container\";\n\nfunction isMutationRecordSavable(record) {\n    return !(record.type === \"attributes\" && record.attributeName === \"placeholder\");\n}\n\nexport class HintPlugin extends Plugin {\n    static id = \"hint\";\n    static dependencies = [\"history\", \"selection\"];\n    resources = {\n        /** Handlers */\n        selectionchange_handlers: this.updateHints.bind(this),\n        external_history_step_handlers: () => {\n            this.clearHints();\n            this.updateHints();\n        },\n        clean_handlers: this.clearHints.bind(this),\n        clean_for_save_handlers: ({ root }) => this.clearHints(root),\n        content_updated_handlers: this.updateHints.bind(this),\n\n        savable_mutation_record_predicates: isMutationRecordSavable,\n        system_classes: [\"o-we-hint\"],\n        ...(this.config.placeholder && {\n            hints: [\n                {\n                    text: this.config.placeholder,\n                    target: (selectionData, editable) => {\n                        if (\n                            selectionData.documentSelectionIsInEditable ||\n                            childNodes(editable).length !== 1\n                        ) {\n                            return;\n                        }\n                        const el = editable.firstChild;\n                        if (isEmptyBlock(el) && el.matches(baseContainerGlobalSelector)) {\n                            return el;\n                        }\n                    },\n                },\n            ],\n        }),\n    };\n\n    setup() {\n        this.hint = null;\n        this.updateHints(this.editable);\n    }\n\n    destroy() {\n        super.destroy();\n        this.clearHints();\n    }\n\n    /**\n     * @param {HTMLElement} [root]\n     */\n    updateHints() {\n        const selectionData = this.dependencies.selection.getSelectionData();\n        const editableSelection = selectionData.editableSelection;\n        if (this.hint) {\n            const blockEl = closestBlock(editableSelection.anchorNode);\n            this.removeHint(this.hint);\n            this.removeHint(blockEl);\n        }\n        if (editableSelection.isCollapsed) {\n            for (const hint of this.getResource(\"hints\")) {\n                if (hint.selector) {\n                    const el = closestBlock(editableSelection.anchorNode);\n                    if (el && el.matches(hint.selector) && !isProtected(el) && isEmptyBlock(el)) {\n                        this.makeHint(el, hint.text);\n                        this.hint = el;\n                    }\n                } else {\n                    const target = hint.target(selectionData, this.editable);\n                    // Do not replace an existing empty block hint by a temp hint.\n                    if (target && !target.classList.contains(\"o-we-hint\")) {\n                        this.makeHint(target, hint.text);\n                        this.hint = target;\n                        return;\n                    }\n                }\n            }\n        }\n    }\n\n    makeHint(el, text) {\n        el.setAttribute(\"placeholder\", text);\n        el.classList.add(\"o-we-hint\");\n    }\n\n    removeHint(el) {\n        el.removeAttribute(\"placeholder\");\n        removeClass(el, \"o-we-hint\");\n        if (this.hint === el) {\n            this.hint = null;\n        }\n    }\n\n    clearHints(root = this.editable) {\n        for (const elem of selectElements(root, \".o-we-hint\")) {\n            this.removeHint(elem);\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { splitTextNode } from \"@html_editor/utils/dom\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { DIRECTIONS } from \"@html_editor/utils/position\";\n\nexport class InlineCodePlugin extends Plugin {\n    static id = \"inlineCode\";\n    static dependencies = [\"selection\", \"history\", \"input\"];\n    resources = {\n        input_handlers: this.onInput.bind(this),\n    };\n\n    onInput(ev) {\n        const selection = this.dependencies.selection.getEditableSelection();\n        if (ev.data !== \"`\" || closestElement(selection.anchorNode, \"code\")) {\n            return;\n        }\n        // We just inserted a backtick, check if there was another\n        // one in the text.\n        let textNode = selection.startContainer;\n        let offset = selection.startOffset;\n        let sibling = textNode.previousSibling;\n        while (sibling && sibling.nodeType === Node.TEXT_NODE) {\n            offset += sibling.textContent.length;\n            sibling.textContent += textNode.textContent;\n            textNode.remove();\n            textNode = sibling;\n            sibling = textNode.previousSibling;\n        }\n        sibling = textNode.nextSibling;\n        while (sibling && sibling.nodeType === Node.TEXT_NODE) {\n            textNode.textContent += sibling.textContent;\n            sibling.remove();\n            sibling = textNode.nextSibling;\n        }\n        this.dependencies.selection.setSelection({ anchorNode: textNode, anchorOffset: offset });\n        const textHasTwoTicks = /`.*`/.test(textNode.textContent);\n        // We don't apply the code tag if there is no content between the two `\n        if (textHasTwoTicks && textNode.textContent.replace(/`/g, \"\").length) {\n            this.dependencies.history.addStep();\n            const insertedBacktickIndex = offset - 1;\n            const textBeforeInsertedBacktick = textNode.textContent.substring(\n                0,\n                insertedBacktickIndex - 1\n            );\n            let startOffset, endOffset;\n            const isClosingForward = textBeforeInsertedBacktick.includes(\"`\");\n            if (isClosingForward) {\n                // There is a backtick before the new backtick.\n                startOffset = textBeforeInsertedBacktick.lastIndexOf(\"`\");\n                endOffset = insertedBacktickIndex;\n            } else {\n                // There is a backtick after the new backtick.\n                const textAfterInsertedBacktick = textNode.textContent.substring(offset);\n                startOffset = insertedBacktickIndex;\n                endOffset = offset + textAfterInsertedBacktick.indexOf(\"`\");\n            }\n            // Split around the backticks if needed so text starts\n            // and ends with a backtick.\n            if (endOffset && endOffset < textNode.textContent.length) {\n                splitTextNode(textNode, endOffset + 1, DIRECTIONS.LEFT);\n            }\n            if (startOffset) {\n                splitTextNode(textNode, startOffset);\n            }\n            // Remove ticks.\n            textNode.textContent = textNode.textContent.substring(\n                1,\n                textNode.textContent.length - 1\n            );\n            // Insert code element.\n            const codeElement = this.document.createElement(\"code\");\n            codeElement.classList.add(\"o_inline_code\");\n            textNode.before(codeElement);\n            codeElement.append(textNode);\n            if (\n                !codeElement.previousSibling ||\n                codeElement.previousSibling.nodeType !== Node.TEXT_NODE\n            ) {\n                codeElement.before(document.createTextNode(\"\\u200B\"));\n            }\n            if (isClosingForward) {\n                // Move selection out of code element.\n                codeElement.after(document.createTextNode(\"\\u200B\"));\n                this.dependencies.selection.setSelection({\n                    anchorNode: codeElement.nextSibling,\n                    anchorOffset: 1,\n                });\n            } else {\n                this.dependencies.selection.setSelection({\n                    anchorNode: codeElement.firstChild,\n                    anchorOffset: 0,\n                });\n            }\n        }\n        this.dependencies.history.addStep();\n    }\n}\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\n\nconst commandCategoryRegistry = registry.category(\"command_categories\");\n// A shortcut conflict occurs when actions are bound to the same\n// shortcut as the command palette. To avoid this, those actions can be\n// added to the command palette itself within this high priority category\n// so that they appear first in the results.\ncommandCategoryRegistry.add(\"shortcut_conflict\", {}, { sequence: 5 });\n", "import { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { URL_REGEX, cleanZWChars } from \"./utils\";\nimport { isImageUrl } from \"@html_editor/utils/url\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { leftPos } from \"@html_editor/utils/position\";\n\nexport class LinkPastePlugin extends Plugin {\n    static id = \"linkPaste\";\n    static dependencies = [\"link\", \"clipboard\", \"selection\", \"dom\"];\n    resources = {\n        before_paste_handlers: this.removeFullySelectedLink.bind(this),\n        paste_text_overrides: this.handlePasteText.bind(this),\n    };\n\n    /**\n     * @param {EditorSelection} selection\n     * @param {string} text\n     */\n    handlePasteText(selection, text) {\n        let splitAroundUrl;\n        // todo: add placeholder plugin that prevent any other plugin\n        // Avoid transforming dynamic placeholder pattern to url.\n        if (!text.match(/\\${.*}/gi)) {\n            splitAroundUrl = text.split(URL_REGEX);\n            // Remove 'http(s)://' capturing group from the result (indexes\n            // 2, 5, 8, ...).\n            splitAroundUrl = splitAroundUrl.filter((_, index) => (index + 1) % 3);\n        }\n        if (\n            !splitAroundUrl ||\n            splitAroundUrl.length < 3 ||\n            closestElement(selection.anchorNode, \"pre\")\n        ) {\n            // Let the default paste handle the text.\n            return false;\n        }\n        if (splitAroundUrl.length === 3 && !splitAroundUrl[0] && !splitAroundUrl[2]) {\n            // Pasted content is a single URL.\n            this.handlePasteTextUrl(selection, text);\n        } else {\n            this.handlePasteTextMultiUrl(selection, splitAroundUrl);\n        }\n        return true;\n    }\n    /**\n     * @param {EditorSelection} selection\n     * @param {string} text\n     */\n    handlePasteTextUrl(selection, text) {\n        const selectionIsInsideALink = !!closestElement(selection.anchorNode, \"a\");\n        const url = /^https?:\\/\\//i.test(text) ? text : \"http://\" + text;\n        if (selectionIsInsideALink) {\n            this.handlePasteTextUrlInsideLink(text, url);\n            return;\n        }\n        if (this.delegateTo(\"paste_url_overrides\", text, url)) {\n            return;\n        }\n        this.dependencies.link.insertLink(url, text);\n    }\n    /**\n     * @param {string} text\n     * @param {string} url\n     */\n    handlePasteTextUrlInsideLink(text, url) {\n        // A url cannot be transformed inside an existing link.\n        // An image can be embedded inside an existing link, a video cannot.\n        if (isImageUrl(url)) {\n            const img = this.document.createElement(\"IMG\");\n            img.setAttribute(\"src\", url);\n            this.dependencies.dom.insert(img);\n        } else {\n            this.dependencies.dom.insert(text);\n        }\n    }\n    /**\n     * @param {EditorSelection} selection\n     * @param {string[]} splitAroundUrl\n     */\n    handlePasteTextMultiUrl(selection, splitAroundUrl) {\n        const selectionIsInsideALink = !!closestElement(selection.anchorNode, \"a\");\n        for (let i = 0; i < splitAroundUrl.length; i++) {\n            const url = /^https?:\\/\\//gi.test(splitAroundUrl[i])\n                ? splitAroundUrl[i]\n                : \"http://\" + splitAroundUrl[i];\n            // Even indexes will always be plain text, and odd indexes will always be URL.\n            // A url cannot be transformed inside an existing link.\n            if (i % 2 && !selectionIsInsideALink) {\n                this.dependencies.dom.insert(\n                    this.dependencies.link.createLink(url, splitAroundUrl[i])\n                );\n            } else if (splitAroundUrl[i] !== \"\") {\n                this.dependencies.clipboard.pasteText(selection, splitAroundUrl[i]);\n            }\n        }\n    }\n\n    /**\n     * @param {EditorSelection} selection\n     */\n    removeFullySelectedLink(selection) {\n        // Replace entire link if its label is fully selected.\n        const link = closestElement(selection.anchorNode, \"a\");\n        if (link && cleanZWChars(selection.textContent()) === cleanZWChars(link.innerText)) {\n            const start = leftPos(link);\n            link.remove();\n            // @doto @phoenix do we still want normalize:false?\n            this.dependencies.selection.setSelection({\n                anchorNode: start[0],\n                anchorOffset: start[1],\n                normalize: false,\n            });\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { cleanTrailingBR, unwrapContents } from \"@html_editor/utils/dom\";\nimport {\n    childNodes,\n    closestElement,\n    descendants,\n    selectElements,\n} from \"@html_editor/utils/dom_traversal\";\nimport { findInSelection, callbacksForCursorUpdate } from \"@html_editor/utils/selection\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { LinkPopover } from \"./link_popover\";\nimport { DIRECTIONS, leftPos, nodeSize, rightPos } from \"@html_editor/utils/position\";\nimport { prepareUpdate } from \"@html_editor/utils/dom_state\";\nimport { EMAIL_REGEX, URL_REGEX, cleanZWChars, deduceURLfromText } from \"./utils\";\nimport { isVisible, isZwnbsp } from \"@html_editor/utils/dom_info\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { memoize } from \"@web/core/utils/functions\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { isBlock, closestBlock } from \"@html_editor/utils/blocks\";\n\n/**\n * @typedef {import(\"@html_editor/core/selection_plugin\").EditorSelection} EditorSelection\n */\n\n/**\n * @param {EditorSelection} selection\n */\nfunction isLinkActive(selection) {\n    const linkElementAnchor = closestElement(selection.anchorNode, \"A\");\n    const linkElementFocus = closestElement(selection.focusNode, \"A\");\n    if (linkElementFocus && linkElementAnchor) {\n        return linkElementAnchor === linkElementFocus;\n    }\n    if (linkElementAnchor || linkElementFocus) {\n        return true;\n    }\n\n    return false;\n}\n\n/**\n * @param {EditorSelection} selection\n */\nfunction isLinkDisabled(selection) {\n    const blockElements = childNodes(selection.commonAncestorContainer).filter(\n        (el) => isBlock(el) && el.isContentEditable\n    );\n    return blockElements.length >= 1;\n}\n\nfunction isSelectionHasLink(selection) {\n    return findInSelection(selection, \"a\") ? true : false;\n}\n\n/**\n * @param { HTMLAnchorElement } link\n * @param {number} offset\n * @returns {\"start\"|\"end\"|false}\n */\nfunction isPositionAtEdgeofLink(link, offset) {\n    const childNodes = [...link.childNodes];\n    let firstVisibleIndex = childNodes.findIndex(isVisible);\n    firstVisibleIndex = firstVisibleIndex === -1 ? 0 : firstVisibleIndex;\n    if (offset <= firstVisibleIndex) {\n        return \"start\";\n    }\n    let lastVisibleIndex = childNodes.reverse().findIndex(isVisible);\n    lastVisibleIndex = lastVisibleIndex === -1 ? 0 : childNodes.length - lastVisibleIndex;\n    if (offset >= lastVisibleIndex) {\n        return \"end\";\n    }\n    return false;\n}\n\nasync function fetchExternalMetaData(url) {\n    // Get the external metadata\n    try {\n        return await rpc(\"/html_editor/link_preview_external\", {\n            preview_url: url,\n        });\n    } catch {\n        // when it's not possible to fetch the metadata we don't want to block the ui\n        return;\n    }\n}\n\nasync function fetchInternalMetaData(url) {\n    // Get the internal metadata\n    const keepLastPromise = new KeepLast();\n    const urlParsed = new URL(url);\n\n    const result = await keepLastPromise\n        .add(fetch(urlParsed))\n        .then((response) => response.text())\n        .then(async (content) => {\n            const html_parser = new window.DOMParser();\n            const doc = html_parser.parseFromString(content, \"text/html\");\n            const internalUrlMetaData = await rpc(\"/html_editor/link_preview_internal\", {\n                preview_url: urlParsed.pathname,\n            });\n\n            internalUrlMetaData[\"favicon\"] = doc.querySelector(\"link[rel~='icon']\");\n            internalUrlMetaData[\"ogTitle\"] = doc.querySelector(\"[property='og:title']\");\n            internalUrlMetaData[\"title\"] = doc.querySelector(\"title\");\n\n            return internalUrlMetaData;\n        })\n        .catch((error) => {\n            // HTTP error codes should not prevent to edit the links, so we\n            // only check for proper instances of Error.\n            if (error instanceof Error) {\n                return Promise.reject(error);\n            }\n        });\n    return result;\n}\n\nasync function fetchAttachmentMetaData(url, ormService) {\n    try {\n        const urlParsed = new URL(url, window.location.origin);\n        const attachementId = parseInt(urlParsed.pathname.split(\"/\").pop());\n        const [{ name, mimetype }] = await ormService.read(\n            \"ir.attachment\",\n            [attachementId],\n            [\"name\", \"mimetype\"]\n        );\n        return { name, mimetype };\n    } catch {\n        return { name: url, mimetype: undefined };\n    }\n}\n\n/**\n * @typedef { Object } LinkShared\n * @property { LinkPlugin['createLink'] } createLink\n * @property { LinkPlugin['getPathAsUrlCommand'] } getPathAsUrlCommand\n * @property { LinkPlugin['insertLink'] } insertLink\n */\n\nexport class LinkPlugin extends Plugin {\n    static id = \"link\";\n    static dependencies = [\n        \"dom\",\n        \"history\",\n        \"input\",\n        \"selection\",\n        \"split\",\n        \"lineBreak\",\n        \"overlay\",\n        \"color\",\n    ];\n    // @phoenix @todo: do we want to have createLink and insertLink methods in link plugin?\n    static shared = [\"createLink\", \"insertLink\", \"getPathAsUrlCommand\"];\n    resources = {\n        user_commands: [\n            {\n                id: \"toggleLinkTools\",\n                title: _t(\"Link\"),\n                description: _t(\"Add a link\"),\n                icon: \"fa-link\",\n                run: this.toggleLinkTools.bind(this),\n            },\n            {\n                id: \"toggleLinkToolsButton\",\n                title: _t(\"Button\"),\n                description: _t(\"Add a button\"),\n                icon: \"fa-link\",\n                run: this.toggleLinkTools.bind(this, { type: \"primary\" }),\n            },\n            {\n                id: \"removeLinkFromSelection\",\n                title: _t(\"Remove Link\"),\n                icon: \"fa-unlink\",\n                isAvailable: isSelectionHasLink,\n                run: this.removeLinkFromSelection.bind(this),\n            },\n        ],\n\n        toolbar_groups: [\n            withSequence(40, { id: \"link\" }),\n            withSequence(30, { id: \"image_link\", namespace: \"image\" }),\n        ],\n        toolbar_items: [\n            {\n                id: \"link\",\n                groupId: \"link\",\n                commandId: \"toggleLinkTools\",\n                isActive: isLinkActive,\n                isDisabled: isLinkDisabled,\n            },\n            {\n                id: \"unlink\",\n                groupId: \"link\",\n                commandId: \"removeLinkFromSelection\",\n            },\n            {\n                id: \"link\",\n                groupId: \"image_link\",\n                commandId: \"toggleLinkTools\",\n                isActive: isLinkActive,\n            },\n            {\n                id: \"unlink\",\n                groupId: \"image_link\",\n                commandId: \"removeLinkFromSelection\",\n            },\n        ],\n\n        powerbox_categories: withSequence(50, { id: \"navigation\", name: _t(\"Navigation\") }),\n        powerbox_items: [\n            {\n                categoryId: \"navigation\",\n                commandId: \"toggleLinkTools\",\n            },\n            {\n                title: _t(\"Button\"),\n                description: _t(\"Add a button\"),\n                categoryId: \"navigation\",\n                commandId: \"toggleLinkToolsButton\",\n            },\n        ],\n\n        power_buttons: { commandId: \"toggleLinkTools\" },\n\n        /** Handlers */\n        beforeinput_handlers: withSequence(5, this.onBeforeInput.bind(this)),\n        selectionchange_handlers: this.handleSelectionChange.bind(this),\n        clean_for_save_handlers: ({ root }) => this.removeEmptyLinks(root),\n        normalize_handlers: this.normalizeLink.bind(this),\n\n        /** Overrides */\n        split_element_block_overrides: this.handleSplitBlock.bind(this),\n        insert_line_break_element_overrides: this.handleInsertLineBreak.bind(this),\n    };\n    setup() {\n        this.overlay = this.dependencies.overlay.createOverlay(LinkPopover, {}, { sequence: 50 });\n        this.addDomListener(this.editable, \"click\", (ev) => {\n            const target = closestElement(ev.target, \"a\");\n            if (target?.isContentEditable) {\n                if (ev.ctrlKey || ev.metaKey) {\n                    window.open(target.href, \"_blank\");\n                }\n                ev.preventDefault();\n                this.toggleLinkTools({ link: target });\n            }\n        });\n        this.addDomListener(this.editable, \"mousedown\", () => {\n            this._isNavigatingByMouse = true;\n        });\n        this.addDomListener(this.editable, \"keydown\", () => {\n            delete this._isNavigatingByMouse;\n        });\n        // link creation is added to the command service because of a shortcut conflict,\n        // as ctrl+k is used for invoking the command palette\n        this.removeLinkShortcut = this.services.command.add(\n            \"Create link\",\n            () => {\n                this.toggleLinkTools();\n                this.dependencies.selection.focusEditable();\n            },\n            {\n                hotkey: \"control+k\",\n                category: \"shortcut_conflict\",\n                isAvailable: () =>\n                    this.dependencies.selection.getSelectionData().documentSelectionIsInEditable,\n            }\n        );\n        this.ignoredClasses = new Set(this.getResource(\"system_classes\"));\n\n        this.getExternalMetaData = memoize(fetchExternalMetaData);\n        this.getInternalMetaData = memoize(fetchInternalMetaData);\n        this.getAttachmentMetadata = memoize((url) =>\n            fetchAttachmentMetaData(url, this.services.orm)\n        );\n    }\n\n    destroy() {\n        this.removeLinkShortcut();\n    }\n\n    // -------------------------------------------------------------------------\n    // Commands\n    // -------------------------------------------------------------------------\n\n    /**\n     * @param {string} url\n     * @param {string} label\n     */\n    createLink(url, label) {\n        const link = this.document.createElement(\"a\");\n        link.setAttribute(\"href\", url);\n        for (const [param, value] of Object.entries(this.config.defaultLinkAttributes || {})) {\n            link.setAttribute(param, `${value}`);\n        }\n        link.innerText = label;\n        return link;\n    }\n    /**\n     * @param {string} url\n     * @param {string} label\n     */\n    insertLink(url, label) {\n        const selection = this.dependencies.selection.getEditableSelection();\n        let link = closestElement(selection.anchorNode, \"a\");\n        if (link) {\n            link.setAttribute(\"href\", url);\n            link.innerText = label;\n        } else {\n            link = this.createLink(url, label);\n            this.dependencies.dom.insert(link);\n        }\n        this.dependencies.history.addStep();\n        const linkParent = link.parentElement;\n        const linkOffset = Array.from(linkParent.childNodes).indexOf(link);\n        this.dependencies.selection.setSelection(\n            { anchorNode: linkParent, anchorOffset: linkOffset + 1 },\n            { normalize: false }\n        );\n    }\n    /**\n     * @param {string} text\n     * @param {string} url\n     */\n    getPathAsUrlCommand(text, url) {\n        const pasteAsURLCommand = {\n            title: _t(\"Paste as URL\"),\n            description: _t(\"Create an URL.\"),\n            icon: \"fa-link\",\n            run: () => {\n                this.dependencies.dom.insert(this.createLink(url, text));\n                this.dependencies.history.addStep();\n            },\n        };\n        return pasteAsURLCommand;\n    }\n    /**\n     * Toggle the Link popover to edit links\n     *\n     * @param {Object} options\n     * @param {HTMLElement} options.link\n     */\n    toggleLinkTools({ link, type } = {}) {\n        if (!link) {\n            link = this.getOrCreateLink();\n        }\n        this.linkElement = link;\n        this.type = type;\n    }\n\n    normalizeLink(root) {\n        const { anchorNode } = this.dependencies.selection.getEditableSelection();\n        const linkEl = closestElement(anchorNode, \"a\");\n        if (linkEl && linkEl.isContentEditable) {\n            const label = linkEl.innerText;\n            const url = deduceURLfromText(label, linkEl);\n            if (url) {\n                linkEl.setAttribute(\"href\", url);\n            }\n        }\n        for (const anchorEl of selectElements(root, \"a\")) {\n            const { color } = anchorEl.style;\n            const childNodes = [...anchorEl.childNodes];\n            // For each anchor element, if it has an inline color style,\n            // (converted from an external style), remove it from the anchor,\n            // create a font tag inside it, and move the color to the font tag.\n            // This ensures the color is applied to the font element instead of\n            // the anchor element itself.\n            if (color && childNodes.every((n) => !isBlock(n))) {\n                anchorEl.style.removeProperty(\"color\");\n                const font = selectElements(anchorEl, \"font\").next().value;\n                if (font && anchorEl.textContent === font.textContent) {\n                    continue;\n                }\n                const newFont = this.document.createElement(\"font\");\n                newFont.append(...childNodes);\n                anchorEl.appendChild(newFont);\n                this.dependencies.color.colorElement(newFont, color, \"color\");\n            }\n        }\n    }\n\n    handleSelectionChange(selectionData) {\n        const selection = selectionData.editableSelection;\n        const props = {\n            onRemove: () => {\n                this.removeLink();\n                this.overlay.close();\n                this.dependencies.history.addStep();\n            },\n            onCopy: () => {\n                this.overlay.close();\n            },\n            onClose: () => {\n                this.overlay.close();\n            },\n            getInternalMetaData: this.getInternalMetaData,\n            getExternalMetaData: this.getExternalMetaData,\n            getAttachmentMetadata: this.getAttachmentMetadata,\n            recordInfo: this.config.getRecordInfo?.() || {},\n            type: this.type || \"\",\n        };\n        if (\n            this._isNavigatingByMouse &&\n            selection.isCollapsed &&\n            selectionData.documentSelectionIsInEditable\n        ) {\n            delete this._isNavigatingByMouse;\n            const { startContainer, startOffset, endContainer, endOffset } = selection;\n            const linkElement = closestElement(startContainer, \"a\");\n            if (\n                linkElement &&\n                linkElement.textContent.startsWith(\"\\uFEFF\") &&\n                linkElement.textContent.endsWith(\"\\uFEFF\")\n            ) {\n                const linkDescendants = descendants(linkElement);\n\n                // Check if the cursor is positioned at the begining of link.\n                const isCursorAtStartOfLink = isZwnbsp(startContainer)\n                    ? linkDescendants.indexOf(startContainer) === 0\n                    : startContainer.nodeType === Node.TEXT_NODE &&\n                      linkDescendants.indexOf(startContainer) === 1 &&\n                      startOffset === 0;\n\n                // Check if the cursor is positioned at the end of link.\n                const isCursorAtEndOfLink = isZwnbsp(endContainer)\n                    ? linkDescendants.indexOf(endContainer) === linkDescendants.length - 1\n                    : endContainer.nodeType === Node.TEXT_NODE &&\n                      linkDescendants.indexOf(endContainer) === linkDescendants.length - 2 &&\n                      endOffset === nodeSize(endContainer);\n\n                // Handle selection movement.\n                if (isCursorAtStartOfLink || isCursorAtEndOfLink) {\n                    const block = closestBlock(linkElement);\n                    const linkIndex = [...block.childNodes].indexOf(linkElement);\n                    this.dependencies.selection.setSelection({\n                        anchorNode: block,\n                        anchorOffset: isCursorAtStartOfLink ? linkIndex - 1 : linkIndex + 2,\n                    });\n                }\n            }\n        }\n        if (!selectionData.documentSelectionIsInEditable) {\n            // note that data-prevent-closing-overlay also used in color picker but link popover\n            // and color picker don't open at the same time so it's ok to query like this\n            const popoverEl = document.querySelector(\"[data-prevent-closing-overlay=true]\");\n            if (popoverEl?.contains(selectionData.documentSelection?.anchorNode)) {\n                return;\n            }\n            this.overlay.close();\n        } else if (!selection.isCollapsed) {\n            const selectedNodes = this.dependencies.selection.getSelectedNodes();\n            const imageNode = selectedNodes.find((node) => node.tagName === \"IMG\");\n            if (imageNode && imageNode.parentNode.tagName === \"A\") {\n                if (this.linkElement !== imageNode.parentElement) {\n                    this.overlay.close();\n                    this.removeCurrentLinkIfEmtpy();\n                }\n                this.linkElement = imageNode.parentElement;\n\n                const imageLinkProps = {\n                    ...props,\n                    isImage: true,\n                    linkEl: this.linkElement,\n                    onApply: (url, _) => {\n                        this.linkElement.href = url;\n                        this.dependencies.selection.setCursorEnd(this.linkElement);\n                        this.dependencies.selection.focusEditable();\n                        this.removeCurrentLinkIfEmtpy();\n                        this.dependencies.history.addStep();\n                    },\n                };\n\n                // close the overlay to always position the popover to the bottom of selected image\n                if (this.overlay.isOpen) {\n                    this.overlay.close();\n                }\n                this.overlay.open({ target: imageNode, props: imageLinkProps });\n            } else {\n                this.overlay.close();\n            }\n        } else {\n            const linkEl = closestElement(selection.anchorNode, \"A\");\n            if (!linkEl) {\n                this.overlay.close();\n                this.removeCurrentLinkIfEmtpy();\n                return;\n            }\n            if (linkEl !== this.linkElement) {\n                this.removeCurrentLinkIfEmtpy();\n                this.overlay.close();\n                this.linkElement = linkEl;\n            }\n\n            // if the link includes an inline image, we close the previous opened popover to reposition it\n            const imageNode = linkEl.querySelector(\"img\");\n            if (imageNode) {\n                this.removeCurrentLinkIfEmtpy();\n                this.overlay.close();\n            }\n\n            if (linkEl.isConnected) {\n                const linkProps = {\n                    ...props,\n                    isImage: false,\n                    linkEl: this.linkElement,\n                    onApply: (url, label, classes) => {\n                        this.linkElement.href = url;\n                        if (cleanZWChars(this.linkElement.innerText) === label) {\n                            this.overlay.close();\n                            this.dependencies.selection.setSelection(\n                                this.dependencies.selection.getEditableSelection()\n                            );\n                        } else {\n                            const restore = prepareUpdate(...leftPos(this.linkElement));\n                            this.linkElement.innerText = label;\n                            restore();\n                            this.overlay.close();\n                            this.dependencies.selection.setCursorEnd(this.linkElement);\n                        }\n                        if (classes) {\n                            this.linkElement.className = classes;\n                        } else {\n                            this.linkElement.removeAttribute(\"class\");\n                        }\n                        cleanTrailingBR(closestBlock(this.linkElement));\n                        this.dependencies.selection.focusEditable();\n                        this.removeCurrentLinkIfEmtpy();\n                        this.dependencies.history.addStep();\n                    },\n                    canEdit: !this.linkElement.classList.contains(\"o_link_readonly\"),\n                    canUpload: !this.config.disableFile,\n                    onUpload: this.config.onAttachmentChange,\n                };\n                // pass the link element to overlay to prevent position change\n                this.overlay.open({ target: this.linkElement, props: linkProps });\n            }\n        }\n    }\n\n    /**\n     * get the link from the selection or create one if there is none\n     *\n     * @return {HTMLElement}\n     */\n    getOrCreateLink() {\n        const selection = this.dependencies.selection.getEditableSelection();\n        const linkElement = findInSelection(selection, \"a\");\n        if (linkElement) {\n            if (\n                !linkElement.contains(selection.anchorNode) ||\n                !linkElement.contains(selection.focusNode)\n            ) {\n                this.dependencies.split.splitSelection();\n                const selectedNodes = this.dependencies.selection.getSelectedNodes();\n                let before = linkElement.previousSibling;\n                while (before !== null && selectedNodes.includes(before)) {\n                    linkElement.insertBefore(before, linkElement.firstChild);\n                    before = linkElement.previousSibling;\n                }\n                let after = linkElement.nextSibling;\n                while (after !== null && selectedNodes.includes(after)) {\n                    linkElement.appendChild(after);\n                    after = linkElement.nextSibling;\n                }\n                this.dependencies.selection.setCursorEnd(linkElement);\n                this.dependencies.history.addStep();\n            }\n            return linkElement;\n        } else {\n            // create a new link element\n            const selectedNodes = this.dependencies.selection.getSelectedNodes();\n            const imageNode = selectedNodes.find((node) => node.tagName === \"IMG\");\n\n            const link = this.document.createElement(\"a\");\n            if (!selection.isCollapsed) {\n                const content = this.dependencies.selection.extractContent(selection);\n                link.append(content);\n                link.normalize();\n            }\n            this.dependencies.dom.insert(link);\n            if (!imageNode) {\n                this.dependencies.selection.setCursorEnd(link);\n            } else {\n                this.dependencies.selection.setSelection({\n                    anchorNode: link,\n                    anchorOffset: 0,\n                    focusNode: link,\n                    focusOffset: nodeSize(link),\n                });\n            }\n            return link;\n        }\n    }\n\n    removeCurrentLinkIfEmtpy() {\n        if (\n            this.linkElement &&\n            cleanZWChars(this.linkElement.innerText) === \"\" &&\n            !this.linkElement.querySelector(\"img\") &&\n            this.linkElement.parentElement?.isContentEditable\n        ) {\n            this.linkElement.remove();\n        }\n        if (\n            this.linkElement &&\n            !this.linkElement.href &&\n            !this.linkElement.hasAttribute(\"t-attf-href\") &&\n            !this.linkElement.hasAttribute(\"t-att-href\")\n        ) {\n            this.removeLink();\n            this.dependencies.history.addStep();\n        }\n    }\n\n    /**\n     * Remove the link from the collapsed selection\n     */\n    removeLink() {\n        const link = this.linkElement;\n        const cursors = this.dependencies.selection.preserveSelection();\n        if (link && link.isContentEditable) {\n            cursors.update(callbacksForCursorUpdate.unwrap(link));\n            unwrapContents(link);\n        }\n        cursors.restore();\n        this.linkElement = null;\n    }\n\n    removeLinkFromSelection() {\n        const selection = this.dependencies.split.splitSelection();\n        const cursors = this.dependencies.selection.preserveSelection();\n\n        // If not, unlink only the part(s) of the link(s) that are selected:\n        // `<a>a[b</a>c<a>d</a>e<a>f]g</a>` => `<a>a</a>[bcdef]<a>g</a>`.\n        let { anchorNode, focusNode, anchorOffset, focusOffset } = selection;\n        const direction = selection.direction;\n        // Split the links around the selection.\n        let [startLink, endLink] = [\n            closestElement(anchorNode, \"a\"),\n            closestElement(focusNode, \"a\"),\n        ];\n        // to remove link from selected images\n        const selectedNodes = this.dependencies.selection.getSelectedNodes();\n        const selectedImageNodes = selectedNodes.filter((node) => node.tagName === \"IMG\");\n        if (selectedImageNodes && startLink && endLink && startLink === endLink) {\n            for (const imageNode of selectedImageNodes) {\n                let imageLink;\n                if (direction === DIRECTIONS.RIGHT) {\n                    imageLink = this.dependencies.split.splitAroundUntil(imageNode, endLink);\n                } else {\n                    imageLink = this.dependencies.split.splitAroundUntil(imageNode, startLink);\n                }\n                cursors.update(callbacksForCursorUpdate.unwrap(imageLink));\n                unwrapContents(imageLink);\n                // update the links at the selection\n                [startLink, endLink] = [\n                    closestElement(anchorNode, \"a\"),\n                    closestElement(focusNode, \"a\"),\n                ];\n            }\n            cursors.restore();\n            // when only unlink an inline image, add step after the unwrapping\n            if (\n                selectedImageNodes.length === 1 &&\n                selectedImageNodes.length === selectedNodes.length\n            ) {\n                this.dependencies.history.addStep();\n                return;\n            }\n        }\n        if (startLink && startLink.isConnected) {\n            anchorNode = this.dependencies.split.splitAroundUntil(anchorNode, startLink);\n            anchorOffset = direction === DIRECTIONS.RIGHT ? 0 : nodeSize(anchorNode);\n            this.dependencies.selection.setSelection(\n                { anchorNode, anchorOffset, focusNode, focusOffset },\n                { normalize: true }\n            );\n        }\n        // Only split the end link if it was not already done above.\n        if (endLink && endLink.isConnected) {\n            focusNode = this.dependencies.split.splitAroundUntil(focusNode, endLink);\n            focusOffset = direction === DIRECTIONS.RIGHT ? nodeSize(focusNode) : 0;\n            this.dependencies.selection.setSelection(\n                { anchorNode, anchorOffset, focusNode, focusOffset },\n                { normalize: true }\n            );\n        }\n        const targetedNodes = this.dependencies.selection.getSelectedNodes();\n        const links = new Set(\n            targetedNodes\n                .map((node) => closestElement(node, \"a\"))\n                .filter((a) => a && a.isContentEditable)\n        );\n        if (links.size) {\n            for (const link of links) {\n                cursors.update(callbacksForCursorUpdate.unwrap(link));\n                unwrapContents(link);\n            }\n            cursors.restore();\n        }\n        this.dependencies.history.addStep();\n    }\n\n    removeEmptyLinks(root) {\n        // @todo: check for unremovables\n        // @todo: preserve spaces\n        for (const link of root.querySelectorAll(\"a\")) {\n            if ([...link.childNodes].some(isVisible)) {\n                continue;\n            }\n            const classes = [...link.classList].filter((c) => !this.ignoredClasses.has(c));\n            const attributes = [...link.attributes].filter(\n                (a) => ![\"style\", \"href\", \"class\"].includes(a.name)\n            );\n            if (!classes.length && !attributes.length && link.parentElement.isContentEditable) {\n                link.remove();\n            }\n        }\n    }\n\n    onBeforeInput(ev) {\n        if (ev.inputType === \"insertParagraph\" || ev.inputType === \"insertLineBreak\") {\n            const nodeForSelectionRestore = this.handleAutomaticLinkInsertion();\n            if (nodeForSelectionRestore) {\n                this.dependencies.selection.setCursorStart(nodeForSelectionRestore);\n                this.dependencies.history.addStep();\n            }\n        }\n        if (ev.inputType === \"insertText\" && ev.data === \" \") {\n            const nodeForSelectionRestore = this.handleAutomaticLinkInsertion();\n            if (nodeForSelectionRestore) {\n                // Since we manually insert a space here, we will be adding a history step\n                // after link creation with selection at the end of the link and another\n                // after inserting the space. So first undo will remove the space, and the\n                // second will undo the link creation.\n                this.dependencies.selection.setSelection({\n                    anchorNode: nodeForSelectionRestore,\n                    anchorOffset: 0,\n                });\n                this.dependencies.history.addStep();\n                nodeForSelectionRestore.textContent =\n                    \"\\u00A0\" + nodeForSelectionRestore.textContent;\n                this.dependencies.selection.setSelection({\n                    anchorNode: nodeForSelectionRestore,\n                    anchorOffset: 1,\n                });\n                this.dependencies.history.addStep();\n                ev.preventDefault();\n            }\n        }\n    }\n    /**\n     * Inserts a link in the editor. Called after pressing space or (shif +) enter.\n     * Performs a regex check to determine if the url has correct syntax.\n     */\n    handleAutomaticLinkInsertion() {\n        let selection = this.dependencies.selection.getEditableSelection();\n        if (\n            isHtmlContentSupported(selection.anchorNode) &&\n            !closestElement(selection.anchorNode, \"a\") &&\n            selection.anchorNode.nodeType === Node.TEXT_NODE\n        ) {\n            // Merge adjacent text nodes.\n            selection.anchorNode.parentNode.normalize();\n            selection = this.dependencies.selection.getEditableSelection();\n            const textSliced = selection.anchorNode.textContent.slice(0, selection.anchorOffset);\n            const textNodeSplitted = textSliced.split(/\\s/);\n            const potentialUrl = textNodeSplitted.pop();\n            // In case of multiple matches, only the last one will be converted.\n            const match = [...potentialUrl.matchAll(new RegExp(URL_REGEX, \"g\"))].pop();\n\n            if (match && !EMAIL_REGEX.test(match[0])) {\n                const nodeForSelectionRestore = selection.anchorNode.splitText(\n                    selection.anchorOffset\n                );\n                const url = match[2] ? match[0] : \"http://\" + match[0];\n                const startOffset = selection.anchorOffset - potentialUrl.length + match.index;\n                const text = selection.anchorNode.textContent.slice(\n                    startOffset,\n                    startOffset + match[0].length\n                );\n                const link = this.createLink(url, text);\n                // split the text node and replace the url text with the link\n                const textNodeToReplace = selection.anchorNode.splitText(startOffset);\n                textNodeToReplace.splitText(match[0].length);\n                selection.anchorNode.parentElement.replaceChild(link, textNodeToReplace);\n                return nodeForSelectionRestore;\n            }\n        }\n    }\n\n    /**\n     * Special behavior for links: do not break the link at its edges, but\n     * rather before/after it.\n     *\n     * @param {Object} params\n     * @param {Element} params.targetNode\n     * @param {number} params.targetOffset\n     * @param {Element} params.blockToSplit\n     */\n    handleSplitBlock(params) {\n        return this.handleEnterAtEdgeOfLink(params, this.dependencies.split.splitElementBlock);\n    }\n\n    /**\n     * Special behavior for links: do not add a line break at its edges, but\n     * rather outside it.\n     *\n     * @param {Object} params\n     * @param {Element} params.targetNode\n     * @param {number} params.targetOffset\n     */\n    handleInsertLineBreak(params) {\n        return this.handleEnterAtEdgeOfLink(\n            params,\n            this.dependencies.lineBreak.insertLineBreakElement\n        );\n    }\n\n    /**\n     * @param {Object} params\n     * @param {Element} params.targetNode\n     * @param {number} params.targetOffset\n     * @param {Element} [params.blockToSplit]\n     * @param {Function} splitOrLineBreakCallback\n     */\n    handleEnterAtEdgeOfLink(params, splitOrLineBreakCallback) {\n        // @todo: handle target Node being a descendent of a link (iterate over\n        // leaves inside the link, rather than childNodes)\n        let { targetNode, targetOffset, blockToSplit } = params;\n        if (targetNode.tagName !== \"A\") {\n            return;\n        }\n        const edge = isPositionAtEdgeofLink(targetNode, targetOffset);\n        if (!edge) {\n            return;\n        }\n        [targetNode, targetOffset] = edge === \"start\" ? leftPos(targetNode) : rightPos(targetNode);\n        blockToSplit = targetNode;\n        splitOrLineBreakCallback({ ...params, targetNode, targetOffset, blockToSplit });\n        return true;\n    }\n}\n\n// @phoenix @todo: duplicate from the clipboard plugin, should be moved to a shared location\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(\n        node,\n        '[data-oe-model]:not([data-oe-field=\"arch\"]):not([data-oe-type=\"html\"]),[data-oe-translation-id]',\n        true\n    );\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Component, useState, onMounted, useRef } from \"@odoo/owl\";\nimport { useAutofocus, useService } from \"@web/core/utils/hooks\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { cleanZWChars, deduceURLfromText } from \"./utils\";\n\nexport class LinkPopover extends Component {\n    static template = \"html_editor.linkPopover\";\n    static props = {\n        linkEl: { validate: (el) => el.nodeType === Node.ELEMENT_NODE },\n        onApply: Function,\n        onRemove: Function,\n        onCopy: Function,\n        onClose: Function,\n        getInternalMetaData: Function,\n        getExternalMetaData: Function,\n        getAttachmentMetadata: Function,\n        isImage: Boolean,\n        type: String,\n        recordInfo: Object,\n        canEdit: { type: Boolean, optional: true },\n        canUpload: { type: Boolean, optional: true },\n        onUpload: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        canEdit: true,\n    };\n    colorsData = [\n        { type: \"\", label: _t(\"Link\"), btnPreview: \"link\" },\n        { type: \"primary\", label: _t(\"Button Primary\"), btnPreview: \"primary\" },\n        { type: \"secondary\", label: _t(\"Button Secondary\"), btnPreview: \"secondary\" },\n        { type: \"custom\", label: _t(\"Custom\"), btnPreview: \"custom\" },\n        // Note: by compatibility the dialog should be able to remove old\n        // colors that were suggested like the BS status colors or the\n        // alpha -> epsilon classes. This is currently done by removing\n        // all btn-* classes anyway.\n    ];\n    buttonSizesData = [\n        { size: \"sm\", label: _t(\"Small\") },\n        { size: \"\", label: _t(\"Medium\") },\n        { size: \"lg\", label: _t(\"Large\") },\n    ];\n    buttonStylesData = [\n        { style: \"fill\", label: _t(\"Fill\") },\n        { style: \"fill,rounded-circle\", label: _t(\"Fill + Rounded\") },\n        { style: \"outline\", label: _t(\"Outline\") },\n        { style: \"outline,rounded-circle\", label: _t(\"Outline + Rounded\") },\n    ];\n    setup() {\n        this.ui = useService(\"ui\");\n        this.notificationService = useService(\"notification\");\n        this.uploadService = useService(\"uploadLocalFiles\");\n\n        this.state = useState({\n            editing: this.props.linkEl.href ? false : true,\n            url: this.props.linkEl.href || \"\",\n            label: cleanZWChars(this.props.linkEl.textContent),\n            previewIcon: {\n                /** @type {'fa'|'imgSrc'|'mimetype'} */\n                type: \"fa\",\n                value: \"fa-globe\",\n            },\n            urlTitle: \"\",\n            urlDescription: \"\",\n            linkPreviewName: \"\",\n            imgSrc: \"\",\n            iconSrc: \"\",\n            classes:\n                this.props.type === \"primary\"\n                    ? \"btn btn-primary\"\n                    : this.props.linkEl.className || \"\",\n            type:\n                this.props.type ||\n                this.props.linkEl.className.match(/btn(-[a-z0-9_-]*)(primary|secondary)/)?.pop() ||\n                \"\",\n            buttonSize: this.props.linkEl.className.match(/btn-(sm|lg)/)?.[1] || \"\",\n            buttonStyle: this.initButtonStyle(this.props.linkEl.className),\n            isImage: this.props.isImage,\n        });\n\n        this.editingWrapper = useRef(\"editing-wrapper\");\n        useAutofocus({\n            refName: this.state.isImage || this.state.label !== \"\" ? \"url\" : \"label\",\n            mobile: true,\n        });\n        onMounted(() => {\n            if (!this.state.editing) {\n                this.loadAsyncLinkPreview();\n            }\n        });\n    }\n    initButtonStyle(className) {\n        const styleArray = [\n            className.match(/btn-([a-z0-9_]+)-(primary|secondary)/)?.[1],\n            className.match(/rounded-circle/)?.pop(),\n        ];\n        return styleArray.every(Boolean)\n            ? styleArray.join(\",\")\n            : styleArray.join(\"\") || className.match(/flat/)?.pop() || \"\";\n    }\n    onClickApply() {\n        this.state.editing = false;\n        if (this.state.label === \"\") {\n            this.state.label = this.state.url;\n        }\n        const deducedUrl = this.deduceUrl(this.state.url);\n        this.state.url = deducedUrl\n            ? this.correctLink(deducedUrl)\n            : this.correctLink(this.state.url);\n        this.loadAsyncLinkPreview();\n        this.props.onApply(this.state.url, this.state.label, this.state.classes);\n    }\n    onClickEdit() {\n        this.state.editing = true;\n        this.state.url = this.props.linkEl.href;\n        this.state.label = cleanZWChars(this.props.linkEl.textContent);\n    }\n    async onClickCopy(ev) {\n        ev.preventDefault();\n        await browser.navigator.clipboard.writeText(this.props.linkEl.href || \"\");\n        this.notificationService.add(_t(\"Link copied to clipboard.\"), {\n            type: \"success\",\n        });\n        this.props.onCopy();\n    }\n    onClickRemove() {\n        this.props.onRemove();\n    }\n\n    onKeydownEnter(ev) {\n        const isAutoCompleteDropdownOpen = document.querySelector(\".o-autocomplete--dropdown-menu\");\n        if (ev.key === \"Enter\" && !isAutoCompleteDropdownOpen) {\n            ev.preventDefault();\n            this.onClickApply();\n        }\n    }\n\n    onKeydown(ev) {\n        if (ev.key === \"Escape\") {\n            ev.preventDefault();\n            this.props.onClose();\n        }\n    }\n\n    onClickReplaceTitle() {\n        this.state.label = this.state.urlTitle;\n        this.onClickApply();\n    }\n\n    /**\n     * @private\n     */\n    correctLink(url) {\n        if (\n            url &&\n            !url.startsWith(\"tel:\") &&\n            !url.startsWith(\"mailto:\") &&\n            !url.includes(\"://\") &&\n            !url.startsWith(\"/\") &&\n            !url.startsWith(\"#\") &&\n            !url.startsWith(\"${\")\n        ) {\n            url = \"http://\" + url;\n        }\n        return url;\n    }\n    deduceUrl(text) {\n        text = text.trim();\n        if (/^(https?:|mailto:|tel:)/.test(text)) {\n            // Text begins with a known protocol, accept it as valid URL.\n            return text;\n        } else {\n            return deduceURLfromText(text, this.props.linkEl) || \"\";\n        }\n    }\n    /**\n     * link preview in the popover\n     */\n    resetPreview() {\n        this.state.previewIcon = { type: \"fa\", value: \"fa-globe\" };\n        this.state.urlTitle = this.state.url || _t(\"No URL specified\");\n        this.state.urlDescription = \"\";\n        this.state.linkPreviewName = \"\";\n    }\n    async loadAsyncLinkPreview() {\n        let url;\n        if (this.state.url === \"\") {\n            this.resetPreview();\n            this.state.previewIcon.value = \"fa-question-circle-o\";\n            return;\n        }\n        if (this.isAttachmentUrl()) {\n            const { name, mimetype } = await this.props.getAttachmentMetadata(this.state.url);\n            this.resetPreview();\n            this.state.urlTitle = name;\n            this.state.previewIcon = { type: \"mimetype\", value: mimetype };\n            return;\n        }\n\n        try {\n            url = new URL(this.state.url); // relative to absolute\n        } catch {\n            // Invalid URL, might happen with editor unsuported protocol. eg type\n            // `geo:37.786971,-122.399677`, become `http://geo:37.786971,-122.399677`\n            this.notificationService.add(_t(\"This URL is invalid. Preview couldn't be updated.\"), {\n                type: \"danger\",\n            });\n            return;\n        }\n        this.resetPreview();\n        const protocol = url.protocol;\n        if (!protocol.startsWith(\"http\")) {\n            const faMap = { \"mailto:\": \"fa-envelope-o\", \"tel:\": \"fa-phone\" };\n            const icon = faMap[protocol];\n            if (icon) {\n                this.state.previewIcon.value = icon;\n            }\n        } else if (window.location.hostname !== url.hostname) {\n            // Preview pages from current website only. External website will\n            // most of the time raise a CORS error. To avoid that error, we\n            // would need to fetch the page through the server (s2s), involving\n            // enduser fetching problematic pages such as illicit content.\n            this.state.previewIcon = {\n                type: \"imgSrc\",\n                value: `https://www.google.com/s2/favicons?sz=16&domain=${encodeURIComponent(url)}`,\n            };\n\n            const externalMetadata = await this.props.getExternalMetaData(this.state.url);\n\n            this.state.urlTitle = externalMetadata?.og_title || this.state.url;\n            this.state.urlDescription = externalMetadata?.og_description || \"\";\n            this.state.imgSrc = externalMetadata?.og_image || \"\";\n            if (\n                externalMetadata?.og_image &&\n                this.state.label &&\n                this.state.urlTitle === this.state.url\n            ) {\n                this.state.urlTitle = this.state.label;\n            }\n        } else {\n            // Set state based on cached link meta data\n            // for record missing errors, we push a warning that the url is likely invalid\n            // for other errors, we log them to not block the ui\n            const internalMetadata = await this.props.getInternalMetaData(this.state.url);\n            if (internalMetadata.favicon) {\n                this.state.previewIcon = {\n                    type: \"imgSrc\",\n                    value: internalMetadata.favicon.href,\n                };\n            }\n            if (internalMetadata.error_msg) {\n                this.notificationService.add(internalMetadata.error_msg, {\n                    type: \"warning\",\n                });\n            } else if (internalMetadata.other_error_msg) {\n                console.error(\n                    \"Internal meta data retrieve error for link preview: \" +\n                        internalMetadata.other_error_msg\n                );\n            } else {\n                this.state.linkPreviewName =\n                    internalMetadata.link_preview_name ||\n                    internalMetadata.display_name ||\n                    internalMetadata.name;\n                this.state.urlDescription = internalMetadata?.description || \"\";\n                this.state.urlTitle = this.state.linkPreviewName\n                    ? this.state.linkPreviewName\n                    : this.state.url;\n            }\n\n            if (\n                (internalMetadata.ogTitle || internalMetadata.title) &&\n                !this.state.linkPreviewName\n            ) {\n                this.state.urlTitle = internalMetadata.ogTitle\n                    ? internalMetadata.ogTitle.getAttribute(\"content\")\n                    : internalMetadata.title.text.trim();\n            }\n        }\n    }\n\n    /**\n     * link style preview in editing mode\n     */\n    onChangeClasses() {\n        const shapes = this.state.buttonStyle ? this.state.buttonStyle.split(\",\") : [];\n        const style = [\"outline\", \"fill\"].includes(shapes[0]) ? `${shapes[0]}-` : \"fill-\";\n        const shapeClasses = shapes.slice(style ? 1 : 0).join(\" \");\n        this.state.classes =\n            (this.state.type ? `btn btn-${style}${this.state.type}` : \"\") +\n            (this.state.type && shapeClasses ? ` ${shapeClasses}` : \"\") +\n            (this.state.type && this.state.buttonSize ? \" btn-\" + this.state.buttonSize : \"\");\n    }\n\n    async uploadFile() {\n        const { upload, getURL } = this.uploadService;\n        const { resModel, resId } = this.props.recordInfo;\n        const [attachment] = await upload({ resModel, resId, accessToken: true });\n        if (!attachment) {\n            // No file selected or upload failed\n            return;\n        }\n        this.props.onUpload?.(attachment);\n        this.state.url = getURL(attachment, { download: true, unique: true, accessToken: true });\n        this.state.label ||= attachment.name;\n    }\n\n    isAttachmentUrl() {\n        return !!this.state.url.match(/\\/web\\/content\\/\\d+/);\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { isBlock } from \"@html_editor/utils/blocks\";\n\nexport class OdooLinkSelectionPlugin extends Plugin {\n    static id = \"odooLinkSelection\";\n    resources = {\n        ineligible_link_for_zwnbsp_predicates: [\n            (link) =>\n                [link, ...link.querySelectorAll(\"*\")].some(\n                    (el) => el.nodeName === \"IMG\" || isBlock(el)\n                ),\n            (link) => link.matches(\"nav a, a.nav-link\"),\n        ],\n        ineligible_link_for_selection_indication_predicates: (link) => link.matches(\".btn\"),\n    };\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { closestElement, selectElements } from \"@html_editor/utils/dom_traversal\";\nimport { removeClass } from \"@html_editor/utils/dom\";\nimport { isProtected, isProtecting, isZwnbsp } from \"@html_editor/utils/dom_info\";\n\n/*\n    This plugin solves selection issues around links (allowing the cursor at the\n    inner and outer edges of links).\n\n    Every link receives 4 zero-width non-breaking spaces (unicode FEFF\n    characters, hereafter referred to as ZWNBSP):\n    - one before the link\n    - one as the link's first child\n    - one as the link's last child\n    - one after the link\n    like so: `//ZWNBSP//<a>//ZWNBSP//label//ZWNBSP//</a>//ZWNBSP`.\n\n    A visual indication ( `o_link_in_selection` class) is added to a link when\n    the selection is contained within it.\n\n    This is not applied in the following cases:\n\n    - in a navbar (since its links are managed via the snippets system, not\n    via pure edition) and, similarly, in .nav-link links\n    - in links that have content more complex than simple text\n    - on non-editable links or links that are not within the editable area\n */\n\n/**\n * @typedef { Object } LinkSelectionShared\n * @property { LinkSelectionPlugin['padLinkWithZwnbsp'] } padLinkWithZwnbsp\n */\n\nexport class LinkSelectionPlugin extends Plugin {\n    static id = \"linkSelection\";\n    static dependencies = [\"selection\", \"feff\"];\n    // TODO ABD: refactor to handle Knowledge comments inside this plugin without sharing padLinkWithZwnbsp.\n    static shared = [\"padLinkWithZwnbsp\"];\n    resources = {\n        /** Handlers */\n        selectionchange_handlers: this.resetLinkInSelection.bind(this),\n        clean_for_save_handlers: ({ root }) => this.clearLinkInSelectionClass(root),\n        normalize_handlers: () => this.resetLinkInSelection(),\n        feff_providers: this.addFeffsToLinks.bind(this),\n        system_classes: [\"o_link_in_selection\"],\n    };\n\n    addFeffsToLinks(root, cursors) {\n        return [...selectElements(root, \"a\")]\n            .filter(this.isLinkEligibleForZwnbsp.bind(this))\n            .flatMap((link) => this.addFeffs(link, cursors));\n    }\n\n    addFeffs(link, cursors) {\n        const addFeff = (position) => {\n            // skip cursor update for append, we want to keep it before the added FEFF\n            const c = position === \"append\" ? null : cursors;\n            return this.dependencies.feff.addFeff(link, position, c);\n        };\n\n        const zwnbspNodes = [];\n        for (const [position, relation] of [\n            [\"before\", \"previousSibling\"],\n            [\"after\", \"nextSibling\"],\n            [\"prepend\", \"firstChild\"],\n            [\"append\", \"lastChild\"],\n        ]) {\n            const candidate = link[relation];\n            const feff = isZwnbsp(candidate) ? candidate : addFeff(position);\n            zwnbspNodes.push(feff);\n        }\n        return zwnbspNodes;\n    }\n\n    /**\n     * Take a link and pad it with non-break zero-width spaces to ensure that it\n     * is always possible to place the cursor at its inner and outer edges.\n     *\n     * @param {HTMLAnchorElement} link\n     */\n    padLinkWithZwnbsp(link) {\n        const cursors = this.dependencies.selection.preserveSelection();\n        this.addFeffs(link, cursors);\n        cursors.restore();\n    }\n\n    isLinkEligibleForZwnbsp(link) {\n        return (\n            link.isContentEditable &&\n            link.parentElement.isContentEditable &&\n            this.editable.contains(link) &&\n            !isProtected(link) &&\n            !isProtecting(link) &&\n            !this.getResource(\"ineligible_link_for_zwnbsp_predicates\").some((p) => p(link))\n        );\n    }\n\n    isLinkEligibleForVisualIndication(link) {\n        return (\n            this.isLinkEligibleForZwnbsp(link) &&\n            !this.getResource(\"ineligible_link_for_selection_indication_predicates\").some(\n                (predicate) => predicate(link)\n            )\n        );\n    }\n\n    /**\n     * Apply the o_link_in_selection class if the selection is in a single link,\n     * remove it otherwise.\n     *\n     * @param {SelectionData} [selectionData]\n     */\n    resetLinkInSelection(selectionData = this.dependencies.selection.getSelectionData()) {\n        this.clearLinkInSelectionClass(this.editable);\n\n        const { anchorNode, focusNode } = selectionData.editableSelection;\n        const [anchorLink, focusLink] = [anchorNode, focusNode].map((node) =>\n            closestElement(node, \"a\")\n        );\n        const singleLinkInSelection = anchorLink === focusLink && anchorLink;\n\n        if (\n            singleLinkInSelection &&\n            this.isLinkEligibleForVisualIndication(singleLinkInSelection)\n        ) {\n            singleLinkInSelection.classList.add(\"o_link_in_selection\");\n        }\n    }\n\n    clearLinkInSelectionClass(root) {\n        for (const link of selectElements(root, \".o_link_in_selection\")) {\n            removeClass(link, \"o_link_in_selection\");\n        }\n    }\n\n    /**\n     * ============================================================= *\n     * The methods below are kept for compatibility (stable policy). *\n     * To be removed in master.                                      *\n     * ============================================================= *\n     */\n\n    /**\n     * @param {Element} root\n     */\n    normalize(root) {\n        this.updateFEFFs(root);\n        this.resetLinkInSelection();\n    }\n\n    /**\n     * @param {Element} root\n     */\n    cleanForSave({ root, preserveSelection = false }) {\n        this.removeFEFFs(root, { preserveSelection });\n        this.clearLinkInSelectionClass(root);\n    }\n\n    /**\n     * @param {Element} root\n     */\n    updateFEFFs(root) {\n        this.dependencies.feff.updateFeffs(root);\n    }\n\n    /**\n     * Removes ZWNBSP characters from text nodes within the given root.\n     *\n     * @param {Element} root\n     * @param {Object} [options]\n     * @param {Function} [options.exclude]\n     */\n    removeFEFFs(root, { exclude = () => false, preserveSelection = true } = {}) {\n        const cursors = preserveSelection ? this.dependencies.selection.preserveSelection() : null;\n        this.dependencies.feff.removeFeffs(root, cursors, { exclude });\n        cursors?.restore();\n    }\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(\n    \"|\"\n)})\\\\b))(?:(?:[/?#])[^\\\\s]*[^!.,})\\\\]'\"\\\\s]|(?:[^!(){}.,[\\\\]'\"\\\\s]+))?`;\nconst httpCapturedRegex = `(https?:\\\\/\\\\/)`;\n\nexport const URL_REGEX = new RegExp(`((?:(?:${httpCapturedRegex}${urlRegexBase})`, \"i\");\nexport const EMAIL_REGEX = /^(mailto:)?[\\w-.]+@(?:[\\w-]+\\.)+[\\w-]{2,4}$/i;\nexport const PHONE_REGEX = /^(tel:(?:\\/\\/)?)?\\+?[\\d\\s.\\-()/]{3,25}$/;\n\nexport function cleanZWChars(text) {\n    return text.replace(/\\u200B|\\uFEFF/g, \"\");\n}\n\n/**\n * Returns a complete URL if text is a valid email address, http URL or telephone\n * number, null otherwise.\n * The optional link parameter is used to prevent protocol switching between\n * 'http' and 'https'.\n *\n * @param {String} text\n * @param {HTMLAnchorElement} [link]\n * @returns {String|null}\n */\nexport function deduceURLfromText(text, link) {\n    const label = cleanZWChars(text).trim();\n    // Check first for e-mail.\n    let match = label.match(EMAIL_REGEX);\n    if (match) {\n        return match[1] ? match[0] : \"mailto:\" + match[0];\n    }\n    // Check for http link.\n    match = label.match(URL_REGEX);\n    if (match && match[0] === label) {\n        const currentHttpProtocol = (link?.href.match(/^http(s)?:\\/\\//gi) || [])[0];\n        if (match[2]) {\n            return match[0];\n        } else if (currentHttpProtocol) {\n            // Avoid converting a http link to https.\n            return currentHttpProtocol + match[0];\n        } else {\n            return \"http://\" + match[0];\n        }\n    }\n    // Check for telephone url.\n    match = label.match(PHONE_REGEX);\n    if (match) {\n        return (match[1] ? match[0] : \"tel:\" + match[0]).replace(/\\s+/g, \"\");\n    }\n    return null;\n}\n\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { closestBlock, isBlock } from \"@html_editor/utils/blocks\";\nimport { removeClass, toggleClass, wrapInlinesInBlocks } from \"@html_editor/utils/dom\";\nimport {\n    getDeepestPosition,\n    isEmptyBlock,\n    isListElement,\n    isListItemElement,\n    isParagraphRelatedElement,\n    isProtected,\n    isProtecting,\n    listElementSelector,\n} from \"@html_editor/utils/dom_info\";\nimport {\n    closestElement,\n    descendants,\n    getAdjacents,\n    selectElements,\n    ancestors,\n    childNodes,\n} from \"@html_editor/utils/dom_traversal\";\nimport { childNodeIndex } from \"@html_editor/utils/position\";\nimport { leftLeafOnlyNotBlockPath } from \"@html_editor/utils/dom_state\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { compareListTypes, createList, insertListAfter, isListItem } from \"./utils\";\nimport { callbacksForCursorUpdate } from \"@html_editor/utils/selection\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { baseContainerGlobalSelector } from \"@html_editor/utils/base_container\";\n\nexport class ListPlugin extends Plugin {\n    static id = \"list\";\n    static dependencies = [\n        \"baseContainer\",\n        \"tabulation\",\n        \"history\",\n        \"input\",\n        \"split\",\n        \"selection\",\n        \"delete\",\n        \"dom\",\n    ];\n    resources = {\n        user_commands: [\n            {\n                id: \"toggleList\",\n                run: this.toggleListCommand.bind(this),\n            },\n            {\n                id: \"toggleListUL\",\n                title: _t(\"Bulleted list\"),\n                description: _t(\"Create a simple bulleted list\"),\n                icon: \"fa-list-ul\",\n                run: () => this.toggleListCommand({ mode: \"UL\" }),\n            },\n            {\n                id: \"toggleListOL\",\n                title: _t(\"Numbered list\"),\n                description: _t(\"Create a list with numbering\"),\n                icon: \"fa-list-ol\",\n                run: () => this.toggleListCommand({ mode: \"OL\" }),\n            },\n            {\n                id: \"toggleListCL\",\n                title: _t(\"Checklist\"),\n                description: _t(\"Track tasks with a checklist\"),\n                icon: \"fa-check-square-o\",\n                run: () => this.toggleListCommand({ mode: \"CL\" }),\n            },\n        ],\n        toolbar_groups: withSequence(30, { id: \"list\" }),\n        toolbar_items: [\n            {\n                id: \"bulleted_list\",\n                groupId: \"list\",\n                commandId: \"toggleListUL\",\n                isActive: this.isListActive(\"UL\"),\n            },\n            {\n                id: \"numbered_list\",\n                groupId: \"list\",\n                commandId: \"toggleListOL\",\n                isActive: this.isListActive(\"OL\"),\n            },\n            {\n                id: \"checklist\",\n                groupId: \"list\",\n                commandId: \"toggleListCL\",\n                isActive: this.isListActive(\"CL\"),\n            },\n        ],\n        powerbox_items: [\n            {\n                categoryId: \"structure\",\n                commandId: \"toggleListUL\",\n            },\n            {\n                categoryId: \"structure\",\n                commandId: \"toggleListOL\",\n            },\n            {\n                categoryId: \"structure\",\n                commandId: \"toggleListCL\",\n            },\n        ],\n        power_buttons: [\n            { commandId: \"toggleListUL\" },\n            { commandId: \"toggleListOL\" },\n            { commandId: \"toggleListCL\" },\n        ],\n\n        hints: [{ selector: \"LI\", text: _t(\"List\") }],\n\n        /** Handlers */\n        input_handlers: this.onInput.bind(this),\n        normalize_handlers: this.normalize.bind(this),\n\n        /** Overrides */\n        delete_backward_overrides: this.handleDeleteBackward.bind(this),\n        delete_range_overrides: this.handleDeleteRange.bind(this),\n        tab_overrides: this.handleTab.bind(this),\n        shift_tab_overrides: this.handleShiftTab.bind(this),\n        split_element_block_overrides: this.handleSplitBlock.bind(this),\n        node_to_insert_processors: this.processNodeToInsert.bind(this),\n    };\n\n    setup() {\n        this.addDomListener(this.editable, \"touchstart\", this.onPointerdown);\n        this.addDomListener(this.editable, \"mousedown\", this.onPointerdown);\n    }\n\n    toggleListCommand({ mode } = {}) {\n        this.toggleList(mode);\n        this.dependencies.history.addStep();\n    }\n\n    onInput(ev) {\n        if (ev.data !== \" \") {\n            return;\n        }\n        const selection = this.dependencies.selection.getEditableSelection();\n        const blockEl = closestBlock(selection.anchorNode);\n        const leftDOMPath = leftLeafOnlyNotBlockPath(selection.anchorNode);\n        let spaceOffset = selection.anchorOffset;\n        let leftLeaf = leftDOMPath.next().value;\n        while (leftLeaf) {\n            // Calculate spaceOffset by adding lengths of previous text nodes\n            // to correctly find offset position for selection within inline\n            // elements. e.g. <p>ab<strong>cd[]e</strong></p>\n            spaceOffset += leftLeaf.length;\n            leftLeaf = leftDOMPath.next().value;\n        }\n        const stringToConvert = blockEl.textContent.substring(0, spaceOffset);\n        const shouldCreateNumberList = /^(?:[1aA])[.)]\\s$/.test(stringToConvert);\n        const shouldCreateBulletList = /^[-*]\\s$/.test(stringToConvert);\n        const shouldCreateCheckList = /^\\[\\]\\s$/.test(stringToConvert);\n        if (\n            (shouldCreateNumberList || shouldCreateBulletList || shouldCreateCheckList) &&\n            !closestElement(selection.anchorNode, \"li\")\n        ) {\n            this.dependencies.selection.setSelection({\n                anchorNode: blockEl.firstChild,\n                anchorOffset: 0,\n                focusNode: selection.focusNode,\n                focusOffset: selection.focusOffset,\n            });\n            this.dependencies.delete.deleteSelection();\n            if (shouldCreateNumberList) {\n                const listStyle = { a: \"lower-alpha\", A: \"upper-alpha\", 1: null }[\n                    stringToConvert.substring(0, 1)\n                ];\n                this.toggleList(\"OL\", listStyle);\n            } else if (shouldCreateBulletList) {\n                this.toggleList(\"UL\");\n            } else if (shouldCreateCheckList) {\n                this.toggleList(\"CL\");\n            }\n            this.dependencies.history.addStep();\n        }\n    }\n\n    // --------------------------------------------------------------------------\n    // Commands\n    // --------------------------------------------------------------------------\n\n    /**\n     * Classifies the selected blocks into three categories:\n     * - LI that are part of a list of the same mode as the target one.\n     * - Lists (UL or OL) that need to have its mode switched to the target mode.\n     * - Blocks that need to be converted to lists.\n     *\n     *  If (and only if) all blocks fall into the first category, the list items\n     *  are converted into paragraphs (result is toggle list OFF).\n     *  Otherwise, the LIs in this category remain unchanged and the other two\n     *  categories are processed.\n     *\n     * @param {string} mode - The list mode to toggle (UL, OL, CL).\n     * @param {string} [listStyle] - The list style ( see listStyle css property)\n     * @throws {Error} If an invalid list type is provided.\n     */\n    toggleList(mode, listStyle) {\n        if (![\"UL\", \"OL\", \"CL\"].includes(mode)) {\n            throw new Error(`Invalid list type: ${mode}`);\n        }\n        if (mode === \"CL\" && !!listStyle) {\n            throw new Error(`listStyle is not compatible with \"CL\" list type`);\n        }\n\n        // @todo @phoenix: original implementation removed whitespace-only text nodes from traversedNodes.\n        // Check if this is necessary.\n\n        const traversedBlocks = this.dependencies.selection.getTraversedBlocks();\n\n        // Keep deepest blocks only.\n        for (const block of traversedBlocks) {\n            if (descendants(block).some((descendant) => traversedBlocks.has(descendant))) {\n                traversedBlocks.delete(block);\n            }\n        }\n\n        // Classify traversed blocks.\n        const sameModeListItems = new Set();\n        const nonListBlocks = new Set();\n        const listsToSwitch = new Set();\n        for (const block of traversedBlocks) {\n            if ([\"OL\", \"UL\"].includes(block.tagName) || !block.isContentEditable) {\n                continue;\n            }\n            const li = closestElement(block, isListItem);\n            if (li) {\n                if (this.getListMode(li.parentElement) === mode) {\n                    sameModeListItems.add(li);\n                } else {\n                    listsToSwitch.add(li.parentElement);\n                }\n            } else {\n                nonListBlocks.add(block);\n            }\n        }\n\n        // Apply changes.\n        if (listsToSwitch.size || nonListBlocks.size) {\n            for (const list of listsToSwitch) {\n                const cursors = this.dependencies.selection.preserveSelection();\n                const newList = this.switchListMode(list, mode);\n                cursors.remapNode(list, newList).restore();\n            }\n            for (const block of nonListBlocks) {\n                const list = this.blockToList(block, mode, listStyle);\n                if (listStyle) {\n                    list.style.listStyle = listStyle;\n                }\n            }\n        } else {\n            for (const li of sameModeListItems) {\n                this.liToBlocks(li);\n            }\n        }\n    }\n\n    normalize(root = this.editable) {\n        const closestNestedLI = closestElement(root, \"li.oe-nested\");\n        if (closestNestedLI) {\n            root = closestNestedLI.parentElement;\n        }\n        for (const element of selectElements(root, \"ul, ol, li\")) {\n            if (isProtected(element) || isProtecting(element)) {\n                continue;\n            }\n            for (const fn of [\n                this.liWithoutParentToP,\n                this.mergeSimilarLists,\n                this.normalizeLI,\n                this.normalizeNestedList,\n            ]) {\n                fn.call(this, element);\n            }\n        }\n    }\n\n    // --------------------------------------------------------------------------\n    // Helpers for toggleList\n    // --------------------------------------------------------------------------\n\n    /**\n     * @param {HTMLElement} element\n     * @param {\"UL\"|\"OL\"|\"CL\"} mode\n     */\n    blockToList(element, mode) {\n        if (element.matches(baseContainerGlobalSelector)) {\n            return this.baseContainerToList(element, mode);\n        }\n        // @todo @phoenix: check for callbacks registered as resources instead?\n        if (element.matches(\"td, th, li.nav-item\")) {\n            return this.blockContentsToList(element, mode);\n        }\n        let list;\n        const cursors = this.dependencies.selection.preserveSelection();\n        if (element === this.editable) {\n            // @todo @phoenix: check if this is needed\n            // Refactor insertListAfter in order to make proper preserveCursor\n            // possible.\n            const callingNode = element.firstChild;\n            const group = getAdjacents(callingNode, (n) => !isBlock(n));\n            list = insertListAfter(this.document, callingNode, mode, [group]);\n        } else {\n            const parent = element.parentNode;\n            const childIndex = childNodeIndex(element);\n            list = insertListAfter(this.document, element, mode, [element]);\n            cursors.update((cursor) => {\n                if (cursor.node === parent) {\n                    if (cursor.offset === childIndex) {\n                        [cursor.node, cursor.offset] = [list.firstChild, 0];\n                    } else if (cursor.offset === childIndex + 1) {\n                        [cursor.node, cursor.offset] = [list.firstChild, 1];\n                    }\n                }\n            });\n            if (element.hasAttribute(\"dir\")) {\n                list.setAttribute(\"dir\", element.getAttribute(\"dir\"));\n            }\n        }\n        cursors.restore();\n        return list;\n    }\n\n    /**\n     * @param {HTMLElement} baseContainer baseContainer Element (can be a div with the\n     *        necessary classes/attributes).\n     * @param {\"UL\"|\"OL\"|\"CL\"} mode\n     */\n    baseContainerToList(baseContainer, mode) {\n        const cursors = this.dependencies.selection.preserveSelection();\n        const list = insertListAfter(this.document, baseContainer, mode, [\n            childNodes(baseContainer),\n        ]);\n        this.dependencies.dom.copyAttributes(baseContainer, list);\n        baseContainer.remove();\n        cursors.remapNode(baseContainer, list.firstChild).restore();\n        return list;\n    }\n\n    blockContentsToList(block, mode) {\n        const cursors = this.dependencies.selection.preserveSelection();\n        const list = insertListAfter(this.document, block.lastChild, mode, [[...block.childNodes]]);\n        cursors.remapNode(block, list.firstChild).restore();\n        return list;\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     * @param {Object} options\n     * @returns {HTMLUListElement|HTMLOListElement|HTMLLIElement} node - Modified\n     * list element after conversion.\n     */\n    convertList(node, newMode) {\n        if (![\"UL\", \"OL\", \"LI\"].includes(node.tagName)) {\n            return;\n        }\n        const listMode = this.getListMode(node);\n        if (listMode && newMode !== listMode) {\n            node = this.switchListMode(node, newMode);\n        }\n        for (const child of node.children) {\n            this.convertList(child, newMode);\n        }\n        return node;\n    }\n\n    getListMode(listContainerEl) {\n        if (![\"UL\", \"OL\"].includes(listContainerEl.tagName)) {\n            return;\n        }\n        if (listContainerEl.tagName === \"OL\") {\n            return \"OL\";\n        }\n        return listContainerEl.classList.contains(\"o_checklist\") ? \"CL\" : \"UL\";\n    }\n\n    isListActive(listMode) {\n        return (selection) => {\n            const block = closestBlock(selection.anchorNode);\n            return block?.tagName === \"LI\" && this.getListMode(block.parentNode) === listMode;\n        };\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     * @param {Object} options\n     * @returns {HTMLOListElement|HTMLUListElement} The modified list element.\n     */\n    switchListMode(list, newMode) {\n        if (this.getListMode(list) === newMode) {\n            return;\n        }\n        const newTag = newMode === \"CL\" ? \"UL\" : newMode;\n        const newList = this.dependencies.dom.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     * Unwraps LI's content into blocks. Equivalent to fully outdenting the LI.\n     *\n     * @param {HTMLLIElement} li\n     */\n    liToBlocks(li) {\n        while (li) {\n            li = this.outdentLI(li);\n        }\n    }\n\n    // --------------------------------------------------------------------------\n    // Helpers for normalize\n    // --------------------------------------------------------------------------\n\n    liWithoutParentToP(element) {\n        const isOrphan = element.nodeName === \"LI\" && !element.closest(\"ul, ol\");\n        if (!isOrphan) {\n            return;\n        }\n        // Transform <li> into <p> if they are not in a <ul> / <ol>.\n        const paragraph = this.dependencies.baseContainer.createBaseContainer();\n        element.replaceWith(paragraph);\n        paragraph.replaceChildren(...element.childNodes);\n    }\n\n    mergeSimilarLists(element) {\n        if (!element.matches(\"ul, ol, li.oe-nested\")) {\n            return;\n        }\n        const previousSibling = element.previousElementSibling;\n        if (\n            previousSibling &&\n            element.isContentEditable &&\n            previousSibling.isContentEditable &&\n            compareListTypes(previousSibling, element)\n        ) {\n            const cursors = this.dependencies.selection.preserveSelection();\n            cursors.update(callbacksForCursorUpdate.merge(element));\n            previousSibling.append(...element.childNodes);\n            // @todo @phoenix: what if unremovable/unmergeable?\n            element.remove();\n\n            cursors.restore();\n        }\n    }\n\n    /**\n     * Wraps inlines in P to avoid inlines with block siblings.\n     */\n    normalizeLI(element) {\n        if (!isListItem(element) || element.classList.contains(\"oe-nested\")) {\n            return;\n        }\n\n        if (\n            [...element.children].some(\n                (child) => isBlock(child) && !this.dependencies.split.isUnsplittable(child)\n            )\n        ) {\n            const cursors = this.dependencies.selection.preserveSelection();\n            wrapInlinesInBlocks(element, {\n                baseContainerNodeName: this.dependencies.baseContainer.getDefaultNodeName(),\n                cursors,\n            });\n            cursors.restore();\n        }\n    }\n\n    normalizeNestedList(element) {\n        if (element.tagName === \"LI\") {\n            return;\n        }\n        if ([\"UL\", \"OL\"].includes(element.parentElement?.tagName)) {\n            const cursors = this.dependencies.selection.preserveSelection();\n            const li = this.document.createElement(\"li\");\n            element.parentElement.insertBefore(li, element);\n            li.appendChild(element);\n            li.classList.add(\"oe-nested\");\n            cursors.restore();\n        }\n    }\n\n    // --------------------------------------------------------------------------\n    // Indentation\n    // --------------------------------------------------------------------------\n\n    // @temp comment: former oTab\n    /**\n     * @param {HTMLLIElement} li\n     */\n    indentLI(li) {\n        const lip = this.document.createElement(\"li\");\n        const parentLi = li.parentElement;\n        const nextSiblingLi = li.nextSibling;\n        lip.classList.add(\"oe-nested\");\n        const destul =\n            li.previousElementSibling?.querySelector(\"ol, ul\") ||\n            li.nextElementSibling?.querySelector(\"ol, ul\") ||\n            li.closest(\"ol, ul\");\n        const cursors = this.dependencies.selection.preserveSelection();\n        // Remove the LI first to force a removal mutation in collaboration.\n        parentLi.removeChild(li);\n        const ul = createList(this.document, this.getListMode(destul));\n        lip.append(ul);\n\n        // lip replaces li\n        li.before(lip);\n        ul.append(li);\n        parentLi.insertBefore(lip, nextSiblingLi);\n        cursors.update((cursor) => {\n            if (cursor.node === lip.parentNode) {\n                const childIndex = childNodeIndex(lip);\n                if (cursor.offset === childIndex) {\n                    [cursor.node, cursor.offset] = [ul, 0];\n                } else if (cursor.offset === childIndex + 1) {\n                    [cursor.node, cursor.offset] = [ul, 1];\n                }\n            }\n        });\n        cursors.restore();\n    }\n\n    // @temp comment: former oShiftTab\n    /**\n     * @param {HTMLLIElement} li\n     * @returns {HTMLLIElement|null} li or null if it no longer exists.\n     */\n    outdentLI(li) {\n        if (li.nextElementSibling) {\n            this.splitList(li.nextElementSibling);\n        }\n\n        if (isListItem(li.parentNode.parentNode)) {\n            this.outdentNestedLI(li);\n            return li;\n        }\n        this.outdentTopLevelLI(li);\n        return null;\n    }\n\n    /**\n     * Splits a list at the given LI element (li is moved to the new list).\n     *\n     * @param {HTMLLIElement} li\n     */\n    splitList(li) {\n        const cursors = this.dependencies.selection.preserveSelection();\n        // Create new list\n        const currentList = li.parentElement;\n        const newList = currentList.cloneNode(false);\n        if (isListItem(li.parentNode.parentNode)) {\n            // li is nested list item\n            const lip = this.document.createElement(\"li\");\n            lip.classList.add(\"oe-nested\");\n            lip.append(newList);\n            cursors.update(callbacksForCursorUpdate.after(li.parentNode.parentNode, lip));\n            li.parentNode.parentNode.after(lip);\n        } else {\n            cursors.update(callbacksForCursorUpdate.after(li.parentNode, newList));\n            li.parentNode.after(newList);\n        }\n        // Move nodes to new list\n        while (li.nextSibling) {\n            cursors.update(callbacksForCursorUpdate.append(newList, li.nextSibling));\n            newList.append(li.nextSibling);\n        }\n        cursors.update(callbacksForCursorUpdate.prepend(newList, li));\n        newList.prepend(li);\n        cursors.restore();\n        return newList;\n    }\n\n    outdentNestedLI(li) {\n        const cursors = this.dependencies.selection.preserveSelection();\n        const ul = li.parentNode;\n        const lip = ul.parentNode;\n        // Move LI\n        cursors.update(callbacksForCursorUpdate.after(lip, li));\n        lip.after(li);\n\n        // Remove UL and LI.oe-nested if left empty.\n        if (!ul.children.length) {\n            cursors.update(callbacksForCursorUpdate.remove(ul));\n            ul.remove();\n        }\n        // @todo @phoenix: not sure in which scenario lip would not have\n        // oe-nested class\n        if (!lip.children.length && lip.classList.contains(\"oe-nested\")) {\n            cursors.update(callbacksForCursorUpdate.remove(lip));\n            lip.remove();\n        }\n        cursors.restore();\n    }\n\n    /**\n     * @param {HTMLLIElement} li\n     */\n    outdentTopLevelLI(li) {\n        const cursors = this.dependencies.selection.preserveSelection();\n        const ul = li.parentNode;\n        const dir = ul.getAttribute(\"dir\");\n        const textAlign = ul.style.getPropertyValue(\"text-align\");\n        wrapInlinesInBlocks(li, {\n            baseContainerNodeName: this.dependencies.baseContainer.getDefaultNodeName(),\n            cursors,\n        });\n        if (!li.hasChildNodes()) {\n            // Outdenting an empty LI produces an empty baseContainer\n            const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n            baseContainer.append(this.document.createElement(\"br\"));\n            li.append(baseContainer);\n            cursors.remapNode(li, baseContainer);\n        }\n        // Move LI's children to after UL\n        for (const block of childNodes(li).reverse()) {\n            if (dir && !block.getAttribute(\"dir\")) {\n                block.setAttribute(\"dir\", dir);\n            }\n            if (textAlign && !block.style.getPropertyValue(\"text-align\")) {\n                block.style.setProperty(\"text-align\", textAlign);\n            }\n            cursors.update(callbacksForCursorUpdate.after(ul, block));\n            ul.after(block);\n        }\n        // Remove LI\n        cursors.update(callbacksForCursorUpdate.remove(li));\n        li.remove();\n        // Remove UL if left empty\n        if (!ul.firstElementChild) {\n            cursors.update(callbacksForCursorUpdate.remove(ul));\n            ul.remove();\n        }\n        cursors.restore();\n    }\n\n    indentListNodes(listNodes) {\n        for (const li of listNodes) {\n            this.indentLI(li);\n        }\n    }\n\n    outdentListNodes(listNodes) {\n        for (const li of listNodes) {\n            this.outdentLI(li);\n        }\n    }\n\n    separateListItems() {\n        const listItems = new Set();\n        const navListItems = new Set();\n        const nonListItems = [];\n        for (const block of this.dependencies.selection.getTraversedBlocks()) {\n            const closestLI = block.closest(\"li\");\n            if (closestLI) {\n                if (closestLI.classList.contains(\"nav-item\")) {\n                    navListItems.add(closestLI);\n                } else if (!closestLI.querySelector(\"li\") && closestLI.isContentEditable) {\n                    // Keep deepest list items only.\n                    listItems.add(closestLI);\n                }\n            } else if (![\"UL\", \"OL\"].includes(block.tagName)) {\n                nonListItems.push(block);\n            }\n        }\n        return { listItems: [...listItems], navListItems: [...navListItems], nonListItems };\n    }\n\n    // --------------------------------------------------------------------------\n    // Handlers of other plugins commands\n    // --------------------------------------------------------------------------\n\n    processNodeToInsert({ nodeToInsert, container }) {\n        if (isListItemElement(container) && isParagraphRelatedElement(nodeToInsert)) {\n            nodeToInsert = this.dependencies.dom.setTagName(nodeToInsert, \"LI\");\n        }\n        const listEl = container && closestElement(container, listElementSelector);\n        if (!listEl) {\n            return nodeToInsert;\n        }\n        const mode = container && this.getListMode(listEl);\n        if (\n            (isListItemElement(nodeToInsert) && nodeToInsert.classList.contains(\"oe-nested\")) ||\n            isListElement(nodeToInsert)\n        ) {\n            return this.convertList(nodeToInsert, mode);\n        }\n        return nodeToInsert;\n    }\n\n    handleTab() {\n        const selection = this.dependencies.selection.getEditableSelection();\n        const closestLI = closestElement(selection.anchorNode, \"LI\");\n        if (closestLI) {\n            const block = closestBlock(selection.anchorNode);\n            const isLiContainsUnSpittable =\n                isParagraphRelatedElement(block) &&\n                ancestors(block, closestLI).find((node) =>\n                    this.dependencies.split.isUnsplittable(node)\n                );\n            if (isLiContainsUnSpittable) {\n                return;\n            }\n        }\n        const { listItems, navListItems, nonListItems } = this.separateListItems();\n        if (listItems.length || navListItems.length) {\n            this.indentListNodes(listItems);\n            this.dependencies.tabulation.indentBlocks(nonListItems);\n            // Do nothing to nav-items.\n            this.dependencies.history.addStep();\n            return true;\n        }\n    }\n\n    handleShiftTab() {\n        const selection = this.dependencies.selection.getEditableSelection();\n        const closestLI = closestElement(selection.anchorNode, \"LI\");\n        if (closestLI) {\n            const block = closestBlock(selection.anchorNode);\n            const isLiContainsUnSpittable =\n                isParagraphRelatedElement(block) &&\n                ancestors(block, closestLI).find((node) =>\n                    this.dependencies.split.isUnsplittable(node)\n                );\n            if (isLiContainsUnSpittable) {\n                return;\n            }\n        }\n        const { listItems, navListItems, nonListItems } = this.separateListItems();\n        if (listItems.length || navListItems.length) {\n            this.outdentListNodes(listItems);\n            this.dependencies.tabulation.outdentBlocks(nonListItems);\n            // Do nothing to nav-items.\n            this.dependencies.history.addStep();\n            return true;\n        }\n    }\n\n    handleSplitBlock(params) {\n        const closestLI = closestElement(params.targetNode, \"LI\");\n        const isBlockUnsplittable =\n            closestLI &&\n            Array.from(closestLI.childNodes).some(\n                (node) => isBlock(node) && this.dependencies.split.isUnsplittable(node)\n            );\n        if (!closestLI || isBlockUnsplittable) {\n            return;\n        }\n        if (isEmptyBlock(closestLI)) {\n            this.outdentLI(closestLI);\n            return true;\n        }\n        const [, newLI] = this.dependencies.split.splitElementBlock({\n            ...params,\n            blockToSplit: closestLI,\n        });\n        if (closestLI.classList.contains(\"o_checked\")) {\n            removeClass(newLI, \"o_checked\");\n        }\n        const [anchorNode, anchorOffset] = getDeepestPosition(newLI, 0);\n        this.dependencies.selection.setSelection({ anchorNode, anchorOffset });\n        return true;\n    }\n\n    /**\n     * Fully outdent list item if cursor is at its beginning.\n     */\n    handleDeleteBackward(range) {\n        const { startContainer, startOffset, endContainer, endOffset } = range;\n        const closestLIendContainer = closestElement(endContainer, \"LI\");\n        if (!closestLIendContainer) {\n            return;\n        }\n        // Detect if cursor is at beginning of LI (or the editable === collapsed range).\n        const isCursorAtStartofLI =\n            (startContainer === endContainer && startOffset === endOffset) ||\n            closestElement(startContainer, \"LI\") !== closestLIendContainer;\n        if (!isCursorAtStartofLI) {\n            return;\n        }\n        // Check if li or parent list(s) are unsplittable.\n        let element = closestLIendContainer;\n        while ([\"LI\", \"UL\", \"OL\"].includes(element.tagName)) {\n            if (this.dependencies.split.isUnsplittable(element)) {\n                return;\n            }\n            element = element.parentElement;\n        }\n        // Fully outdent LI.\n        this.liToBlocks(closestLIendContainer);\n        return true;\n    }\n\n    // Uncheck checklist item left empty after deleting a multi-LI selection.\n    handleDeleteRange(range) {\n        const { startContainer, endContainer } = range;\n        const startCheckedLi = closestElement(startContainer, \"li.o_checked\");\n        if (!startCheckedLi) {\n            return;\n        }\n        const endLi = closestElement(endContainer, \"li\");\n        if (startCheckedLi === endLi) {\n            return;\n        }\n\n        range = this.dependencies.delete.deleteRange(range);\n        this.dependencies.selection.setSelection({\n            anchorNode: range.startContainer,\n            anchorOffset: range.startOffset,\n        });\n\n        if (isEmptyBlock(startCheckedLi)) {\n            removeClass(startCheckedLi, \"o_checked\");\n        }\n\n        return true;\n    }\n\n    // --------------------------------------------------------------------------\n    // Event handlers\n    // --------------------------------------------------------------------------\n\n    /**\n     * @param {MouseEvent | TouchEvent} ev\n     */\n    onPointerdown(ev) {\n        const node = ev.target;\n        const isChecklistItem =\n            node.tagName == \"LI\" && this.getListMode(node.parentElement) == \"CL\";\n        if (!isChecklistItem) {\n            return;\n        }\n        let offsetX = ev.offsetX;\n        let offsetY = ev.offsetY;\n        if (ev.type === \"touchstart\") {\n            const rect = node.getBoundingClientRect();\n            offsetX = ev.touches[0].clientX - rect.x;\n            offsetY = ev.touches[0].clientY - rect.y;\n        }\n\n        if (isChecklistItem && this.isPointerInsideCheckbox(node, offsetX, offsetY)) {\n            toggleClass(node, \"o_checked\");\n            ev.preventDefault();\n            this.dependencies.history.addStep();\n        }\n    }\n\n    /**\n     * @param {MouseEvent} ev\n     * @param {HTMLLIElement} li - LI element inside a checklist.\n     */\n    isPointerInsideCheckbox(li, pointerOffsetX, pointerOffsetY) {\n        const beforeStyle = this.document.defaultView.getComputedStyle(li, \":before\");\n        const checkboxPosition = {\n            left: parseInt(beforeStyle.left),\n            top: parseInt(beforeStyle.top),\n        };\n        checkboxPosition.right = checkboxPosition.left + parseInt(beforeStyle.width);\n        checkboxPosition.bottom = checkboxPosition.top + parseInt(beforeStyle.height);\n\n        return (\n            pointerOffsetX >= checkboxPosition.left &&\n            pointerOffsetX <= checkboxPosition.right &&\n            pointerOffsetY >= checkboxPosition.top &&\n            pointerOffsetY <= checkboxPosition.bottom\n        );\n    }\n}\n", "export function createList(document, 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\n// @todo @phoenix Change this API (and implementation), as all use cases seem to\n// create a list with a single LI\nexport function insertListAfter(document, afterNode, mode, content = []) {\n    const list = createList(document, 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\n/* Returns true if the two lists are of the same type among:\n * - OL\n * - regular UL\n * - checklist (ul.o_checklist)\n * - container for nested lists (li.oe-nested)\n */\nexport function compareListTypes(a, b) {\n    if (!a || !b || a.tagName !== b.tagName) {\n        return false;\n    }\n    if (a.classList.contains(\"o_checklist\") !== b.classList.contains(\"o_checklist\")) {\n        return false;\n    }\n    if (a.tagName === \"LI\") {\n        if (a.classList.contains(\"oe-nested\") !== b.classList.contains(\"oe-nested\")) {\n            return false;\n        }\n        return compareListTypes(a.firstElementChild, b.firstElementChild);\n    }\n    return true;\n}\n\nexport function isListItem(node) {\n    return node.nodeName === \"LI\" && !node.classList.contains(\"nav-item\");\n}\n", "import { Plugin } from \"../plugin\";\n\n/**\n * @typedef { Object } LocalOverlayShared\n * @property { LocalOverlayPlugin['makeLocalOverlay'] } makeLocalOverlay\n */\n\n/**\n * This plugins provides a way to create a \"local\" overlays so that their\n * visibility is relative to the overflow of their ancestors.\n */\nexport class LocalOverlayPlugin extends Plugin {\n    static id = \"localOverlay\";\n    static shared = [\"makeLocalOverlay\"];\n\n    setup() {\n        this.localOverlayContainer = this.config.localOverlayContainers?.ref.el;\n    }\n\n    /**\n     * Make a local container to organise floating elements inside it's own\n     * box and z-index isolation.\n     *\n     * @param {string} containerId An id to add to the container in order to make\n     *              the container more visible in the devtool and potentially\n     *              add css rules for the container and it's children.\n     */\n    makeLocalOverlay(containerId) {\n        const container = this.document.createElement(\"div\");\n        container.className = `oe-local-overlay`;\n        container.setAttribute(\"data-oe-local-overlay-id\", containerId);\n        if (this.localOverlayContainer) {\n            this.localOverlayContainer.append(container);\n        }\n        return container;\n    }\n}\n", "import {\n    DocumentSelector,\n    renderStaticFileBox,\n} from \"@html_editor/main/media/media_dialog/document_selector\";\nimport { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class FilePlugin extends Plugin {\n    static id = \"file\";\n    static dependencies = [\"dom\", \"history\"];\n    resources = {\n        user_commands: {\n            id: \"uploadFile\",\n            title: _t(\"Upload a file\"),\n            description: _t(\"Add a download box\"),\n            icon: \"fa-upload\",\n            run: this.uploadAndInsertFiles.bind(this),\n            isAvailable: this.isUploadCommandAvailable.bind(this),\n        },\n        powerbox_items: {\n            categoryId: \"media\",\n            commandId: \"uploadFile\",\n            keywords: [_t(\"file\"), _t(\"document\")],\n        },\n        power_buttons: withSequence(5, { commandId: \"uploadFile\" }),\n        unsplittable_node_predicates: (node) => node.classList?.contains(\"o_file_box\"),\n        ...(!this.config.disableFile && {\n            media_dialog_extra_tabs: {\n                id: \"DOCUMENTS\",\n                title: _t(\"Documents\"),\n                Component: this.componentForMediaDialog,\n                sequence: 15,\n            },\n        }),\n        selectors_for_feff_providers: () => \".o_file_box\",\n    };\n\n    get recordInfo() {\n        return this.config.getRecordInfo?.() || {};\n    }\n\n    isUploadCommandAvailable() {\n        return !this.config.disableFile;\n    }\n\n    get componentForMediaDialog() {\n        return DocumentSelector;\n    }\n\n    async uploadAndInsertFiles() {\n        // Upload\n        const attachments = await this.services.uploadLocalFiles.upload(this.recordInfo, {\n            multiple: true,\n            accessToken: true,\n        });\n        if (!attachments.length) {\n            // No files selected or error during upload\n            this.editable.focus();\n            return;\n        }\n        if (this.config.onAttachmentChange) {\n            attachments.forEach(this.config.onAttachmentChange);\n        }\n        // Render\n        const fileCards = attachments.map(this.renderDownloadBox.bind(this));\n        // Insert\n        fileCards.forEach(this.dependencies.dom.insert);\n        this.dependencies.history.addStep();\n    }\n\n    renderDownloadBox(attachment) {\n        const url = this.services.uploadLocalFiles.getURL(attachment, {\n            download: true,\n            unique: true,\n            accessToken: true,\n        });\n        const { name: filename, mimetype } = attachment;\n        return renderStaticFileBox(filename, mimetype, url);\n    }\n}\n", "import { withSequence } from \"@html_editor/utils/resource\";\nimport { Plugin } from \"../../plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ColorSelector } from \"../font/color_selector\";\n\nexport class IconPlugin extends Plugin {\n    static id = \"icon\";\n    static dependencies = [\"history\", \"selection\", \"color\"];\n    resources = {\n        user_commands: [\n            {\n                id: \"resizeIcon1\",\n                title: _t(\"Icon size 1x\"),\n                run: () => this.resizeIcon({ size: \"1\" }),\n            },\n            {\n                id: \"resizeIcon2\",\n                title: _t(\"Icon size 2x\"),\n                run: () => this.resizeIcon({ size: \"2\" }),\n            },\n            {\n                id: \"resizeIcon3\",\n                title: _t(\"Icon size 3x\"),\n                run: () => this.resizeIcon({ size: \"3\" }),\n            },\n            {\n                id: \"resizeIcon4\",\n                title: _t(\"Icon size 4x\"),\n                run: () => this.resizeIcon({ size: \"4\" }),\n            },\n            {\n                id: \"resizeIcon5\",\n                title: _t(\"Icon size 5x\"),\n                run: () => this.resizeIcon({ size: \"5\" }),\n            },\n            {\n                id: \"toggleSpinIcon\",\n                title: _t(\"Toggle icon spin\"),\n                icon: \"fa-play\",\n                run: this.toggleSpinIcon.bind(this),\n            },\n        ],\n        toolbar_namespaces: [\n            {\n                id: \"icon\",\n                isApplied: (traversedNodes) =>\n                    traversedNodes.every(\n                        (node) =>\n                            // All nodes should be icons, its ZWS child or its ancestors\n                            node.classList?.contains(\"fa\") ||\n                            node.parentElement.classList.contains(\"fa\") ||\n                            (node.querySelector?.(\".fa\") && node.isContentEditable !== false)\n                    ),\n            },\n        ],\n        toolbar_groups: [\n            withSequence(1, {\n                id: \"icon_color\",\n                namespace: \"icon\",\n            }),\n            withSequence(1, {\n                id: \"icon_size\",\n                namespace: \"icon\",\n            }),\n            withSequence(3, { id: \"icon_spin\", namespace: \"icon\" }),\n        ],\n        toolbar_items: [\n            {\n                id: \"icon_forecolor\",\n                groupId: \"icon_color\",\n                title: _t(\"Font Color\"),\n                Component: ColorSelector,\n                props: this.dependencies.color.getPropsForColorSelector(\"foreground\"),\n            },\n            {\n                id: \"icon_backcolor\",\n                groupId: \"icon_color\",\n                title: _t(\"Background Color\"),\n                Component: ColorSelector,\n                props: this.dependencies.color.getPropsForColorSelector(\"background\"),\n            },\n            {\n                id: \"icon_size_1\",\n                groupId: \"icon_size\",\n                commandId: \"resizeIcon1\",\n                text: \"1x\",\n                isActive: () => this.hasIconSize(\"1\"),\n            },\n            {\n                id: \"icon_size_2\",\n                groupId: \"icon_size\",\n                commandId: \"resizeIcon2\",\n                text: \"2x\",\n                isActive: () => this.hasIconSize(\"2\"),\n            },\n            {\n                id: \"icon_size_3\",\n                groupId: \"icon_size\",\n                commandId: \"resizeIcon3\",\n                text: \"3x\",\n                isActive: () => this.hasIconSize(\"3\"),\n            },\n            {\n                id: \"icon_size_4\",\n                groupId: \"icon_size\",\n                commandId: \"resizeIcon4\",\n                text: \"4x\",\n                isActive: () => this.hasIconSize(\"4\"),\n            },\n            {\n                id: \"icon_size_5\",\n                groupId: \"icon_size\",\n                commandId: \"resizeIcon5\",\n                text: \"5x\",\n                isActive: () => this.hasIconSize(\"5\"),\n            },\n            {\n                id: \"icon_spin\",\n                groupId: \"icon_spin\",\n                commandId: \"toggleSpinIcon\",\n                isActive: () => this.hasSpinIcon(),\n            },\n        ],\n        color_apply_overrides: this.applyIconColor.bind(this),\n    };\n\n    getSelectedIcon() {\n        const selectedNodes = this.dependencies.selection.getSelectedNodes();\n        return selectedNodes.find((node) => node.classList?.contains?.(\"fa\"));\n    }\n\n    resizeIcon({ size }) {\n        const selectedIcon = this.getSelectedIcon();\n        if (!selectedIcon) {\n            return;\n        }\n        for (const classString of selectedIcon.classList) {\n            if (classString.match(/^fa-[2-5]x$/)) {\n                selectedIcon.classList.remove(classString);\n            }\n        }\n        if (size !== \"1\") {\n            selectedIcon.classList.add(`fa-${size}x`);\n        }\n        this.dependencies.history.addStep();\n    }\n\n    toggleSpinIcon() {\n        const selectedIcon = this.getSelectedIcon();\n        if (!selectedIcon) {\n            return;\n        }\n        selectedIcon.classList.toggle(\"fa-spin\");\n    }\n\n    hasIconSize(size) {\n        const selectedIcon = this.getSelectedIcon();\n        if (!selectedIcon) {\n            return;\n        }\n        if (size === \"1\") {\n            return ![...selectedIcon.classList].some((classString) =>\n                classString.match(/^fa-[2-5]x$/)\n            );\n        }\n        return selectedIcon.classList.contains(`fa-${size}x`);\n    }\n\n    hasSpinIcon() {\n        const selectedIcon = this.getSelectedIcon();\n        if (!selectedIcon) {\n            return;\n        }\n        return selectedIcon.classList.contains(\"fa-spin\");\n    }\n\n    applyIconColor(color, mode) {\n        const selectedIcon = this.getSelectedIcon();\n        if (!selectedIcon) {\n            return;\n        }\n        this.dependencies.color.colorElement(selectedIcon, color, mode);\n        return true;\n    }\n}\n", "import {\n    applyModifications,\n    cropperDataFields,\n    activateCropper,\n    loadImage,\n    loadImageInfo,\n} from \"@html_editor/utils/image_processing\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport {\n    Component,\n    useRef,\n    onMounted,\n    onWillDestroy,\n    markup,\n    useExternalListener,\n} from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { scrollTo, closestScrollableY } from \"@web/core/utils/scrolling\";\n\nexport class ImageCrop extends Component {\n    static template = \"html_editor.ImageCrop\";\n    static props = {\n        document: { validate: (p) => p.nodeType === Node.DOCUMENT_NODE },\n        media: { optional: true },\n        mimetype: { type: String, optional: true },\n        onClose: { type: Function, optional: true },\n        onSave: { type: Function, optional: true },\n    };\n\n    setup() {\n        this.aspectRatios = {\n            \"0/0\": { label: _t(\"Flexible\"), value: 0 },\n            \"16/9\": { label: \"16:9\", value: 16 / 9 },\n            \"4/3\": { label: \"4:3\", value: 4 / 3 },\n            \"1/1\": { label: \"1:1\", value: 1 },\n            \"2/3\": { label: \"2:3\", value: 2 / 3 },\n        };\n        this.notification = useService(\"notification\");\n        this.media = this.props.media;\n        this.document = this.props.document;\n\n        this.elRef = useRef(\"el\");\n        this.cropperWrapper = useRef(\"cropperWrapper\");\n        this.imageRef = useRef(\"imageRef\");\n        this.cropperOpen = false;\n\n        // We use capture so that the handler is called before other editor handlers\n        // like save, such that we can restore the src before a save.\n        // We need to add event listeners to the owner document of the widget.\n        useExternalListener(this.document, \"mousedown\", this.onDocumentMousedown, {\n            capture: true,\n        });\n        useExternalListener(this.document, \"keydown\", this.onDocumentKeydown, {\n            capture: true,\n        });\n\n        onMounted(() => {\n            this.hasModifiedImageClass = this.media.classList.contains(\"o_modified_image_to_save\");\n            if (this.hasModifiedImageClass) {\n                this.media.classList.remove(\"o_modified_image_to_save\");\n            }\n            this.show();\n        });\n        onWillDestroy(this.closeCropper);\n    }\n\n    closeCropper() {\n        if (!this.cropperOpen) {\n            return;\n        }\n        this.cropper?.destroy?.();\n        this.media.setAttribute(\"src\", this.initialSrc);\n        if (\n            this.hasModifiedImageClass &&\n            !this.media.classList.contains(\"o_modified_image_to_save\")\n        ) {\n            this.media.classList.add(\"o_modified_image_to_save\");\n        }\n        this.props?.onClose?.();\n        this.cropperOpen = false;\n    }\n\n    /**\n     * Resets the crop\n     */\n    async reset() {\n        if (this.cropper) {\n            this.cropper.reset();\n            if (this.aspectRatio !== \"0/0\") {\n                this.aspectRatio = \"0/0\";\n                this.cropper.setAspectRatio(this.aspectRatios[this.aspectRatio].value);\n            }\n            await this.save(false);\n        }\n    }\n\n    async show() {\n        if (this.cropperOpen) {\n            return;\n        }\n        // key: ratio identifier, label: displayed to user, value: used by cropper lib\n        const src = this.media.getAttribute(\"src\");\n        const data = { ...this.media.dataset };\n        this.initialSrc = src;\n        this.aspectRatio = data.aspectRatio || \"0/0\";\n        const mimetype =\n            data.mimetype || src.endsWith(\".png\")\n                ? \"image/png\"\n                : src.endsWith(\".webp\")\n                ? \"image/webp\"\n                : \"image/jpeg\";\n        this.mimetype = this.props.mimetype || mimetype;\n\n        await loadImageInfo(this.media);\n        const isIllustration = /^\\/(?:html|web)_editor\\/shape\\/illustration\\//.test(\n            this.media.dataset.originalSrc\n        );\n        this.uncroppable = false;\n        if (this.media.dataset.originalSrc && !isIllustration) {\n            this.originalSrc = this.media.dataset.originalSrc;\n            this.originalId = this.media.dataset.originalId;\n        } else {\n            // Couldn't find an attachment: not croppable.\n            this.uncroppable = true;\n        }\n\n        if (this.uncroppable) {\n            this.notification.add(\n                markup(\n                    _t(\n                        \"This type of image is not supported for cropping.<br/>If you want to crop it, please first download it from the original source and upload it in Odoo.\"\n                    )\n                ),\n                {\n                    title: _t(\"This image is an external image\"),\n                    type: \"warning\",\n                }\n            );\n            return this.closeCropper();\n        }\n\n        await this.scrollToInvisibleImage();\n        // Replacing the src with the original's so that the layout is correct.\n        await loadImage(this.originalSrc, this.media);\n        const cropperImage = this.imageRef.el;\n        [cropperImage.style.width, cropperImage.style.height] = [\n            this.media.width + \"px\",\n            this.media.height + \"px\",\n        ];\n\n        const sel = this.document.getSelection();\n        sel && sel.removeAllRanges();\n\n        // Overlaying the cropper image over the real image\n        let offset = undefined;\n        if (!this.media.getClientRects().length) {\n            offset = { top: 0, left: 0 };\n        } else {\n            const rect = this.media.getBoundingClientRect();\n            const win = this.media.ownerDocument.defaultView;\n            offset = {\n                top: rect.top + win.pageYOffset,\n                left: rect.left + win.pageXOffset,\n            };\n        }\n\n        offset.left += parseInt(this.media.style.paddingLeft || 0);\n        offset.top += parseInt(this.media.style.paddingRight || 0);\n        const frameElement = this.media.ownerDocument.defaultView.frameElement;\n        if (frameElement) {\n            const frameRect = frameElement.getBoundingClientRect();\n            offset.left += frameRect.left;\n            offset.top += frameRect.top;\n        }\n\n        this.cropperWrapper.el.style.left = `${offset.left}px`;\n        this.cropperWrapper.el.style.top = `${offset.top}px`;\n\n        await loadImage(this.originalSrc, cropperImage);\n\n        this.cropper = await activateCropper(\n            cropperImage,\n            this.aspectRatios[this.aspectRatio].value,\n            this.media.dataset\n        );\n        this.cropperOpen = true;\n    }\n    /**\n     * Updates the DOM image with cropped data and associates required\n     * information for a potential future save (where required cropped data\n     * attachments will be created).\n     *\n     * @private\n     * @param {boolean} [cropped=true]\n     */\n    async save(cropped = true) {\n        // Mark the media for later creation of cropped attachment\n        this.media.classList.add(\"o_modified_image_to_save\");\n\n        [...cropperDataFields, \"aspectRatio\"].forEach((attr) => {\n            delete this.media.dataset[attr];\n            const value = this.getAttributeValue(attr);\n            if (value) {\n                this.media.dataset[attr] = value;\n            }\n        });\n        delete this.media.dataset.resizeWidth;\n        this.initialSrc = await applyModifications(this.media, this.cropper, {\n            forceModification: true,\n            mimetype: this.mimetype,\n        });\n        this.media.classList.toggle(\"o_we_image_cropped\", cropped);\n        this.closeCropper();\n        this.props.onSave?.();\n    }\n    /**\n     * Returns an attribute's value for saving.\n     *\n     * @private\n     */\n    getAttributeValue(attr) {\n        if (cropperDataFields.includes(attr)) {\n            return this.cropper.getData()[attr];\n        }\n        return this[attr];\n    }\n    /**\n     * Resets the crop box to prevent it going outside the image.\n     *\n     * @private\n     */\n    resetCropBox() {\n        this.cropper.clear();\n        this.cropper.crop();\n    }\n    /**\n     * Make sure the targeted image is in the visible viewport before crop.\n     *\n     * @private\n     */\n    async scrollToInvisibleImage() {\n        const rect = this.media.getBoundingClientRect();\n        const viewportTop = this.document.documentElement.scrollTop || 0;\n        const viewportBottom = viewportTop + window.innerHeight;\n        // Give priority to the closest scrollable element (e.g. for images in\n        // HTML fields, the element to scroll is different from the document's\n        // scrolling element).\n        const scrollable = closestScrollableY(this.media);\n\n        // The image must be in a position that allows access to it and its crop\n        // options buttons. Otherwise, the crop widget container can be scrolled\n        // to allow editing.\n        if (rect.top < viewportTop || viewportBottom - rect.bottom < 100) {\n            await scrollTo(this.media, {\n                behavior: \"smooth\",\n                ...(scrollable && { scrollable }),\n            });\n        }\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    onZoom(scale) {\n        this.cropper.zoom(scale);\n    }\n\n    onReset() {\n        this.cropper.reset();\n    }\n\n    onRotate(degree) {\n        this.cropper.rotate(degree);\n        this.resetCropBox();\n    }\n\n    onFlip(scaleDirection) {\n        const amount = this.cropper.getData()[scaleDirection] * -1;\n        this.cropper[scaleDirection](amount);\n    }\n\n    setAspectRatio(ratio) {\n        this.cropper.reset();\n        this.aspectRatio = ratio;\n        this.cropper.setAspectRatio(this.aspectRatios[this.aspectRatio].value);\n    }\n\n    /**\n     * Discards crop if the user clicks outside of the widget.\n     *\n     * @private\n     * @param {MouseEvent} ev\n     */\n    onDocumentMousedown(ev) {\n        if (\n            this.props.document.body.contains(ev.target) &&\n            (this.elRef.el === ev.target || !this.elRef.el.contains(ev.target))\n        ) {\n            return this.closeCropper();\n        }\n    }\n    /**\n     * Save crop if user hits enter,\n     * discard crop on escape.\n     *\n     * @private\n     * @param {KeyboardEvent} ev\n     */\n    onDocumentKeydown(ev) {\n        if (ev.key === \"Enter\") {\n            return this.save();\n        } else if (ev.key === \"Escape\") {\n            ev.stopImmediatePropagation();\n            return this.closeCropper();\n        }\n    }\n    /**\n     * Resets the cropbox on zoom to prevent crop box overflowing.\n     *\n     * @private\n     */\n    async onCropZoom() {\n        // Wait for the zoom event to be fully processed before reseting.\n        await new Promise((res) => setTimeout(res, 0));\n        this.resetCropBox();\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { Plugin } from \"../../plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ImageCrop } from \"./image_crop\";\nimport { loadBundle } from \"@web/core/assets\";\nimport { withSequence } from \"@html_editor/utils/resource\";\n\nexport class ImageCropPlugin extends Plugin {\n    static id = \"imageCrop\";\n    static dependencies = [\"selection\", \"history\"];\n    resources = {\n        user_commands: [\n            {\n                id: \"cropImage\",\n                run: this.openCropImage.bind(this),\n                title: _t(\"Crop image\"),\n                icon: \"fa-crop\",\n            },\n        ],\n        toolbar_groups: withSequence(27, {\n            id: \"image_crop\",\n            namespace: \"image\",\n        }),\n        toolbar_items: [\n            {\n                id: \"image_crop\",\n                commandId: \"cropImage\",\n                groupId: \"image_crop\",\n            },\n        ],\n    };\n\n    setup() {\n        this.imageCropProps = {\n            media: undefined,\n            mimetype: undefined,\n        };\n    }\n\n    getSelectedImage() {\n        const selectedNodes = this.dependencies.selection.getSelectedNodes();\n        return selectedNodes.find((node) => node.tagName === \"IMG\");\n    }\n\n    async openCropImage() {\n        const selectedImg = this.getSelectedImage();\n        if (!selectedImg) {\n            return;\n        }\n\n        this.imageCropProps.media = selectedImg;\n\n        const onClose = () => {\n            registry.category(\"main_components\").remove(\"ImageCropping\");\n        };\n\n        const onSave = () => {\n            this.dependencies.history.addStep();\n        };\n\n        await loadBundle(\"html_editor.assets_image_cropper\");\n\n        registry.category(\"main_components\").add(\"ImageCropping\", {\n            Component: ImageCrop,\n            props: { ...this.imageCropProps, onClose, onSave, document: this.document },\n        });\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { toolbarButtonProps } from \"@html_editor/main/toolbar/toolbar\";\n\nexport class ImageDescription extends Component {\n    static components = { Dialog };\n    static props = {\n        getDescription: Function,\n        getTooltip: Function,\n        updateImageDescription: Function,\n        ...toolbarButtonProps,\n    };\n    static template = \"html_editor.ImageDescription\";\n\n    setup() {\n        this.dialog = useService(\"dialog\");\n    }\n\n    openDescriptionDialog() {\n        this.dialog.add(ImageDescriptionDialog, {\n            description: this.props.getDescription(),\n            onConfirm: (description, tooltip) =>\n                this.props.updateImageDescription({ description, tooltip }),\n            tooltip: this.props.getTooltip(),\n        });\n    }\n}\n\nclass ImageDescriptionDialog extends Component {\n    static components = { Dialog };\n    static props = {\n        close: Function,\n        description: {\n            type: String,\n            optional: true,\n        },\n        onConfirm: Function,\n        tooltip: {\n            type: String,\n            optional: true,\n        },\n    };\n    static template = \"html_editor.ImageDescriptionDialog\";\n\n    setup() {\n        this.state = {\n            description: this.props.description,\n            tooltip: this.props.tooltip,\n        };\n    }\n\n    onSave() {\n        this.props.onConfirm(this.state.description, this.state.tooltip);\n        this.props.close();\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { toolbarButtonProps } from \"@html_editor/main/toolbar/toolbar\";\n\nexport class ImagePadding extends Component {\n    static components = { Dropdown, DropdownItem };\n    static props = {\n        ...toolbarButtonProps,\n        onSelected: Function,\n    };\n    static template = \"html_editor.ImagePadding\";\n\n    setup() {\n        this.paddings = { None: 0, Small: 1, Medium: 2, Large: 3, XL: 5 };\n    }\n\n    onSelected(padding) {\n        this.props.onSelected({ size: this.paddings[padding] });\n    }\n}\n", "import { Plugin } from \"../../plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { isImageUrl } from \"@html_editor/utils/url\";\nimport { ImageDescription } from \"./image_description\";\nimport { ImagePadding } from \"./image_padding\";\nimport { createFileViewer } from \"@web/core/file_viewer/file_viewer_hook\";\nimport { boundariesOut, childNodeIndex } from \"@html_editor/utils/position\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { ImageTransformButton } from \"./image_transform_button\";\nimport { isEmpty } from \"@html_editor/utils/dom_info\";\n\nfunction hasShape(imagePlugin, shapeName) {\n    return () => imagePlugin.isSelectionShaped(shapeName);\n}\n\nexport class ImagePlugin extends Plugin {\n    static id = \"image\";\n    static dependencies = [\"history\", \"link\", \"powerbox\", \"dom\", \"selection\"];\n    resources = {\n        user_commands: [\n            {\n                id: \"deleteImage\",\n                title: _t(\"Remove (DELETE)\"),\n                icon: \"fa-trash text-danger\",\n                run: this.deleteImage.bind(this),\n            },\n            {\n                id: \"previewImage\",\n                title: _t(\"Preview image\"),\n                icon: \"fa-search-plus\",\n                run: this.previewImage.bind(this),\n            },\n            {\n                id: \"setImageShapeRounded\",\n                title: _t(\"Shape: Rounded\"),\n                icon: \"fa-square\",\n                run: () => this.setImageShape(\"rounded\", { excludeClasses: [\"rounded-circle\"] }),\n            },\n            {\n                id: \"setImageShapeCircle\",\n                title: _t(\"Shape: Circle\"),\n                icon: \"fa-circle-o\",\n                run: () => this.setImageShape(\"rounded-circle\", { excludeClasses: [\"rounded\"] }),\n            },\n            {\n                id: \"setImageShapeShadow\",\n                title: _t(\"Shape: Shadow\"),\n                icon: \"fa-sun-o\",\n                run: () => this.setImageShape(\"shadow\"),\n            },\n            {\n                id: \"setImageShapeThumbnail\",\n                title: _t(\"Shape: Thumbnail\"),\n                icon: \"fa-picture-o\",\n                run: () => this.setImageShape(\"img-thumbnail\"),\n            },\n            { id: \"resizeImage\", run: this.resizeImage.bind(this) },\n        ],\n        toolbar_namespaces: [\n            {\n                id: \"image\",\n                isApplied: (traversedNodes) =>\n                    traversedNodes.every(\n                        // All nodes should be images or its ancestors\n                        (node) => node.nodeName === \"IMG\" || node.querySelector?.(\"img\")\n                    ),\n            },\n        ],\n        toolbar_groups: [\n            withSequence(23, { id: \"image_preview\", namespace: \"image\" }),\n            withSequence(24, { id: \"image_description\", namespace: \"image\" }),\n            withSequence(25, { id: \"image_shape\", namespace: \"image\" }),\n            withSequence(26, { id: \"image_padding\", namespace: \"image\" }),\n            withSequence(26, { id: \"image_size\", namespace: \"image\" }),\n            withSequence(26, { id: \"image_transform\", namespace: \"image\" }),\n            withSequence(30, { id: \"image_delete\", namespace: \"image\" }),\n        ],\n        toolbar_items: [\n            {\n                id: \"image_preview\",\n                groupId: \"image_preview\",\n                commandId: \"previewImage\",\n            },\n            {\n                id: \"image_description\",\n                title: _t(\"Edit media description\"),\n                groupId: \"image_description\",\n                Component: ImageDescription,\n                props: {\n                    getDescription: () => this.getImageAttribute(\"alt\"),\n                    getTooltip: () => this.getImageAttribute(\"title\"),\n                    updateImageDescription: this.updateImageDescription.bind(this),\n                },\n            },\n            {\n                id: \"shape_rounded\",\n                groupId: \"image_shape\",\n                commandId: \"setImageShapeRounded\",\n                isActive: hasShape(this, \"rounded\"),\n            },\n            {\n                id: \"shape_circle\",\n                groupId: \"image_shape\",\n                commandId: \"setImageShapeCircle\",\n                isActive: hasShape(this, \"rounded-circle\"),\n            },\n            {\n                id: \"shape_shadow\",\n                groupId: \"image_shape\",\n                commandId: \"setImageShapeShadow\",\n                isActive: hasShape(this, \"shadow\"),\n            },\n            {\n                id: \"shape_thumbnail\",\n                groupId: \"image_shape\",\n                commandId: \"setImageShapeThumbnail\",\n                isActive: hasShape(this, \"img-thumbnail\"),\n            },\n            {\n                id: \"image_padding\",\n                groupId: \"image_padding\",\n                title: _t(\"Image padding\"),\n                Component: ImagePadding,\n                props: {\n                    onSelected: this.setImagePadding.bind(this),\n                },\n            },\n            {\n                id: \"resize_default\",\n                groupId: \"image_size\",\n                commandId: \"resizeImage\",\n                title: _t(\"Resize Default\"),\n                text: _t(\"Default\"),\n                isActive: () => this.hasImageSize(\"\"),\n            },\n            {\n                id: \"resize_100\",\n                groupId: \"image_size\",\n                commandId: \"resizeImage\",\n                commandParams: { size: \"100%\" },\n                title: _t(\"Resize Full\"),\n                text: \"100%\",\n                isActive: () => this.hasImageSize(\"100%\"),\n            },\n            {\n                id: \"resize_50\",\n                groupId: \"image_size\",\n                commandId: \"resizeImage\",\n                commandParams: { size: \"50%\" },\n                title: _t(\"Resize Half\"),\n                text: \"50%\",\n                isActive: () => this.hasImageSize(\"50%\"),\n            },\n            {\n                id: \"resize_25\",\n                groupId: \"image_size\",\n                commandId: \"resizeImage\",\n                commandParams: { size: \"25%\" },\n                title: _t(\"Resize Quarter\"),\n                text: \"25%\",\n                isActive: () => this.hasImageSize(\"25%\"),\n            },\n            {\n                id: \"image_transform\",\n                groupId: \"image_transform\",\n                title: _t(\"Transform the picture (click twice to reset transformation)\"),\n                Component: ImageTransformButton,\n                props: this.getImageTransformProps(),\n            },\n            {\n                id: \"image_delete\",\n                groupId: \"image_delete\",\n                commandId: \"deleteImage\",\n            },\n        ],\n        paste_url_overrides: this.handlePasteUrl.bind(this),\n    };\n\n    setup() {\n        this.addDomListener(this.editable, \"dblclick\", (e) => {\n            if (e.target.tagName === \"IMG\") {\n                this.previewImage();\n            }\n        });\n        this.addDomListener(this.editable, \"pointerup\", (e) => {\n            if (e.target.tagName === \"IMG\") {\n                const [anchorNode, anchorOffset, focusNode, focusOffset] = boundariesOut(e.target);\n                this.dependencies.selection.setSelection({\n                    anchorNode,\n                    anchorOffset,\n                    focusNode,\n                    focusOffset,\n                });\n                this.dependencies.selection.focusEditable();\n            }\n        });\n        this.fileViewer = createFileViewer();\n    }\n\n    destroy() {\n        super.destroy();\n    }\n\n    setImagePadding({ size } = {}) {\n        const selectedImg = this.getSelectedImage();\n        if (!selectedImg) {\n            return;\n        }\n        for (const classString of selectedImg.classList) {\n            if (classString.match(/^p-[0-9]$/)) {\n                selectedImg.classList.remove(classString);\n            }\n        }\n        selectedImg.classList.add(`p-${size}`);\n        this.dependencies.history.addStep();\n    }\n    resizeImage({ size } = {}) {\n        const selectedImg = this.getSelectedImage();\n        if (!selectedImg) {\n            return;\n        }\n        selectedImg.style.width = size || \"\";\n        this.dependencies.history.addStep();\n    }\n\n    setImageShape(className, { excludeClasses = [] } = {}) {\n        const selectedImg = this.getSelectedImage();\n        if (!selectedImg) {\n            return;\n        }\n        for (const classString of excludeClasses) {\n            if (selectedImg.classList.contains(classString)) {\n                selectedImg.classList.remove(classString);\n            }\n        }\n        selectedImg.classList.toggle(className);\n        this.dependencies.history.addStep();\n    }\n\n    previewImage() {\n        const selectedImg = this.getSelectedImage();\n        if (!selectedImg) {\n            return;\n        }\n        const fileModel = {\n            isImage: true,\n            isViewable: true,\n            displayName: selectedImg.src,\n            defaultSource: selectedImg.src,\n            downloadUrl: selectedImg.src,\n        };\n        this.document.getSelection().collapseToEnd();\n        this.fileViewer.open(fileModel);\n    }\n\n    deleteImage() {\n        const selectedImg = this.getSelectedImage();\n        if (selectedImg) {\n            const anchorNode = selectedImg.parentElement;\n            let anchorOffset = childNodeIndex(selectedImg);\n            selectedImg.remove();\n            // When an image is added as the first element of a <p> tag,\n            // the `dom_plugin.insert` method automatically creates a #text node just before the <img>.\n            // After removing the image and setting the selection at the <p> tag (offset 0),\n            // the selection unexpectedly jumps back to the parent node during input.\n            // To address this issue, we handle this specific case separately.\n            if (anchorNode.nodeName === \"P\" && isEmpty(anchorNode)) {\n                const br = this.document.createElement(\"br\");\n                anchorNode.replaceChildren(br);\n                anchorOffset = 0;\n            }\n            this.dependencies.selection.setSelection({ anchorNode, anchorOffset });\n            this.dependencies.history.addStep();\n        }\n    }\n\n    getSelectedImage() {\n        const selectedNodes = this.dependencies.selection.getSelectedNodes();\n        return selectedNodes.find((node) => node.tagName === \"IMG\");\n    }\n\n    hasImageSize(size) {\n        const selectedImg = this.getSelectedImage();\n        return selectedImg?.style?.width === size;\n    }\n\n    isSelectionShaped(shape) {\n        const selectedNodes = this.dependencies.selection\n            .getTraversedNodes()\n            .filter((n) => n.tagName === \"IMG\" && n.classList.contains(shape));\n        return selectedNodes.length > 0;\n    }\n\n    getImageAttribute(attributeName) {\n        const selectedNodes = this.dependencies.selection.getSelectedNodes();\n        const selectedImg = selectedNodes.find((node) => node.tagName === \"IMG\");\n        return selectedImg.getAttribute(attributeName) || undefined;\n    }\n\n    /**\n     * @param {string} text\n     * @param {string} url\n     */\n    handlePasteUrl(text, url) {\n        if (isImageUrl(url)) {\n            const restoreSavepoint = this.dependencies.history.makeSavePoint();\n            // Open powerbox with commands to embed media or paste as link.\n            // Insert URL as text, revert it later if a command is triggered.\n            this.dependencies.dom.insert(text);\n            this.dependencies.history.addStep();\n            const embedImageCommand = {\n                title: _t(\"Embed Image\"),\n                description: _t(\"Embed the image in the document.\"),\n                icon: \"fa-image\",\n                run: () => {\n                    const img = document.createElement(\"IMG\");\n                    img.setAttribute(\"src\", url);\n                    this.dependencies.dom.insert(img);\n                    this.dependencies.history.addStep();\n                },\n            };\n            const commands = [\n                embedImageCommand,\n                this.dependencies.link.getPathAsUrlCommand(text, url),\n            ];\n            this.dependencies.powerbox.openPowerbox({ commands, onApplyCommand: restoreSavepoint });\n            return true;\n        }\n    }\n\n    updateImageDescription({ description, tooltip } = {}) {\n        const selectedImg = this.getSelectedImage();\n        if (!selectedImg) {\n            return;\n        }\n        selectedImg.setAttribute(\"alt\", description);\n        selectedImg.setAttribute(\"title\", tooltip);\n        this.dependencies.history.addStep();\n    }\n\n    resetImageTransformation(image) {\n        image.setAttribute(\n            \"style\",\n            (image.getAttribute(\"style\") || \"\").replace(/[^;]*transform[\\w:]*;?/g, \"\")\n        );\n        this.dependencies.history.addStep();\n    }\n\n    getImageTransformProps() {\n        return {\n            icon: \"fa-object-ungroup\",\n            getSelectedImage: this.getSelectedImage.bind(this),\n            resetImageTransformation: this.resetImageTransformation.bind(this),\n            addStep: this.dependencies.history.addStep.bind(this),\n            document: this.document,\n            editable: this.editable,\n        };\n    }\n}\n", "import { Component, useExternalListener, useState } from \"@odoo/owl\";\nimport { toolbarButtonProps } from \"@html_editor/main/toolbar/toolbar\";\nimport { registry } from \"@web/core/registry\";\nimport { ImageTransformation } from \"./image_transformation\";\n\nexport class ImageTransformButton extends Component {\n    static template = \"html_editor.ImageTransformButton\";\n    static props = {\n        icon: String,\n        getSelectedImage: Function,\n        resetImageTransformation: Function,\n        addStep: Function,\n        document: { validate: (p) => p.nodeType === Node.DOCUMENT_NODE },\n        editable: { validate: (p) => p.nodeType === Node.ELEMENT_NODE },\n        ...toolbarButtonProps,\n    };\n\n    setup() {\n        this.state = useState({ active: false });\n        this.mouseDownInsideTransform = false;\n        // We close the image transform when we click outside any element not related to it\n        // When the mousedown of the click is inside the image transform and mouseup is outside\n        // while resizing or rotating the image it will consider the click as being done outside\n        // image transform. So we need to keep track if the mousedown is inside or outside to\n        // know if we want to close the image transform component or not.\n        useExternalListener(this.props.document, \"mousedown\", (ev) => {\n            if (this.isNodeInsideTransform(ev.target)) {\n                this.mouseDownInsisdeTransform = true;\n            } else {\n                this.closeImageTransformation();\n                this.mouseDownInsideTransform = false;\n            }\n        });\n        useExternalListener(this.props.document, \"click\", (ev) => {\n            if (!this.isNodeInsideTransform(ev.target) && !this.mouseDownInsideTransform) {\n                this.closeImageTransformation();\n            }\n            this.mouseDownInsideTransform = false;\n        });\n        // When we click on any character the image is deleted and we need to close the image transform\n        // We handle this by selectionchange\n        useExternalListener(this.props.document, \"selectionchange\", (ev) => {\n            this.closeImageTransformation();\n        });\n    }\n\n    isNodeInsideTransform(node) {\n        if (!node) {\n            return false;\n        }\n        if (node.nodeType === Node.TEXT_NODE) {\n            node = node.parentElement;\n        }\n        if (node.matches('[name=\"image_transform\"], [name=\"image_transform\"] *')) {\n            return true;\n        }\n        if (\n            this.isImageTransformationOpen() &&\n            node.matches(\n                \".transfo-container, .transfo-container div, .transfo-container i, .transfo-container span\"\n            )\n        ) {\n            return true;\n        }\n        return false;\n    }\n\n    onButtonClick() {\n        this.handleImageTransformation(this.props.getSelectedImage());\n    }\n\n    handleImageTransformation(image) {\n        if (this.isImageTransformationOpen()) {\n            this.props.resetImageTransformation(image);\n            this.closeImageTransformation();\n        } else {\n            this.openImageTransformation(image);\n        }\n    }\n\n    openImageTransformation(image) {\n        this.state.active = true;\n        registry.category(\"main_components\").add(\"ImageTransformation\", {\n            Component: ImageTransformation,\n            props: {\n                image,\n                document: this.props.document,\n                editable: this.props.editable,\n                destroy: () => this.closeImageTransformation(),\n                onChange: () => this.props.addStep(),\n            },\n        });\n    }\n\n    isImageTransformationOpen() {\n        return registry.category(\"main_components\").contains(\"ImageTransformation\");\n    }\n\n    closeImageTransformation() {\n        this.state.active = false;\n        if (this.isImageTransformationOpen()) {\n            registry.category(\"main_components\").remove(\"ImageTransformation\");\n        }\n    }\n}\n", "/*\nCopyright (c) 2014 Christophe Matthieu,\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this software and associated documentation\nfiles (the \"Software\"), to deal in the Software without\nrestriction, including without limitation the rights to use,\ncopy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\nHOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\nOTHER DEALINGS IN THE SOFTWARE.\n*/\n\nimport { Component, onMounted, useExternalListener, useRef } from \"@odoo/owl\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { usePositionHook } from \"@html_editor/position_hook\";\n\nconst rad = Math.PI / 180;\n\nexport class ImageTransformation extends Component {\n    static template = \"html_editor.ImageTransformation\";\n    static props = {\n        document: { validate: (p) => p.nodeType === Node.DOCUMENT_NODE },\n        editable: { validate: (p) => p.nodeType === Node.ELEMENT_NODE },\n        image: { validate: (p) => p.tagName === \"IMG\" },\n        destroy: { type: Function },\n        onChange: { type: Function },\n    };\n\n    setup() {\n        this.document = this.props.document;\n        this.image = this.props.image;\n        this.transfoContainer = useRef(\"transfoContainer\");\n        this.transfoControls = useRef(\"transfoControls\");\n        this.transfoCenter = useRef(\"transfoCenter\");\n        this.computeImageTransformations();\n        onMounted(() => {\n            this.positionTransfoContainer();\n        });\n        useExternalListener(window, \"mousemove\", this.mouseMove);\n        useExternalListener(window, \"mouseup\", this.mouseUp);\n        useHotkey(\"escape\", () => this.props.destroy());\n        usePositionHook({ el: this.props.editable }, this.document, this.resetHandlers);\n    }\n\n    mouseMove(ev) {\n        if (!this.transfo.active) {\n            return;\n        }\n        ev.preventDefault();\n        const settings = this.transfo.settings;\n        const center = this.transfo.active.center;\n        const cdx = center.left - ev.pageX;\n        const cdy = center.top - ev.pageY;\n\n        if (this.transfo.active.type == \"rotator\") {\n            let ang;\n            const dang =\n                Math.atan(\n                    (settings.width * settings.scalex) / (settings.height * settings.scaley)\n                ) / rad;\n\n            if (cdy) {\n                ang = Math.atan(-cdx / cdy) / rad;\n            } else {\n                ang = 0;\n            }\n            if (ev.pageY >= center.top && ev.pageX >= center.left) {\n                ang += 180;\n            } else if (ev.pageY >= center.top && ev.pageX < center.left) {\n                ang += 180;\n            } else if (ev.pageY < center.top && ev.pageX < center.left) {\n                ang += 360;\n            }\n\n            ang -= dang;\n            if (settings.scaley < 0 && settings.scalex < 0) {\n                ang += 180;\n            }\n\n            if (!ev.ctrlKey) {\n                settings.angle =\n                    Math.round(ang / this.transfo.settings.rotationStep) *\n                    this.transfo.settings.rotationStep;\n            } else {\n                settings.angle = ang;\n            }\n\n            // reset position : don't move center\n            this.positionTransfoContainer();\n            const new_center = this.getOffset(this.transfoCenter.el);\n            const x = center.left - new_center.left;\n            const y = center.top - new_center.top;\n            const angle = ang * rad;\n            settings.translatex += x * Math.cos(angle) - y * Math.sin(-angle);\n            settings.translatey += -x * Math.sin(angle) + y * Math.cos(-angle);\n        } else if (this.transfo.active.type == \"position\") {\n            const angle = settings.angle * rad;\n            const x = ev.pageX - this.transfo.active.pageX;\n            const y = ev.pageY - this.transfo.active.pageY;\n            this.transfo.active.pageX = ev.pageX;\n            this.transfo.active.pageY = ev.pageY;\n            const dx = x * Math.cos(angle) - y * Math.sin(-angle);\n            const dy = -x * Math.sin(angle) + y * Math.cos(-angle);\n\n            settings.translatex += dx;\n            settings.translatey += dy;\n        } else if (this.transfo.active.type.length === 2) {\n            const angle = settings.angle * rad;\n            const dx = cdx * Math.cos(angle) - cdy * Math.sin(-angle);\n            const dy = -cdx * Math.sin(angle) + cdy * Math.cos(-angle);\n            if (this.transfo.active.type.indexOf(\"t\") != -1) {\n                settings.scaley = dy / (settings.height / 2);\n            }\n            if (this.transfo.active.type.indexOf(\"b\") != -1) {\n                settings.scaley = -dy / (settings.height / 2);\n            }\n            if (this.transfo.active.type.indexOf(\"l\") != -1) {\n                settings.scalex = dx / (settings.width / 2);\n            }\n            if (this.transfo.active.type.indexOf(\"r\") != -1) {\n                settings.scalex = -dx / (settings.width / 2);\n            }\n            if (settings.scaley > 0 && settings.scaley < 0.05) {\n                settings.scaley = 0.05;\n            }\n            if (settings.scalex > 0 && settings.scalex < 0.05) {\n                settings.scalex = 0.05;\n            }\n            if (settings.scaley < 0 && settings.scaley > -0.05) {\n                settings.scaley = -0.05;\n            }\n            if (settings.scalex < 0 && settings.scalex > -0.05) {\n                settings.scalex = -0.05;\n            }\n\n            if (\n                ev.shiftKey &&\n                (this.transfo.active.type === \"tl\" ||\n                    this.transfo.active.type === \"bl\" ||\n                    this.transfo.active.type === \"tr\" ||\n                    this.transfo.active.type === \"br\")\n            ) {\n                settings.scaley = settings.scalex;\n            }\n        }\n\n        settings.angle = Math.round(settings.angle);\n        settings.translatex = Math.round(settings.translatex);\n        settings.translatey = Math.round(settings.translatey);\n        settings.scalex = Math.round(settings.scalex * 100) / 100;\n        settings.scaley = Math.round(settings.scaley * 100) / 100;\n        this.positionTransfoContainer();\n        this.props.onChange();\n    }\n\n    mouseUp() {\n        this.transfo.active = null;\n    }\n\n    mouseDown(ev) {\n        if (this.transfo.active) {\n            return;\n        }\n        let type = \"position\";\n        const target = ev.target.closest(\"div\");\n\n        if (target.classList.contains(\"transfo-rotator\")) {\n            type = \"rotator\";\n        } else if (target.classList.contains(\"transfo-scaler-tl\")) {\n            type = \"tl\";\n        } else if (target.classList.contains(\"transfo-scaler-tr\")) {\n            type = \"tr\";\n        } else if (target.classList.contains(\"transfo-scaler-br\")) {\n            type = \"br\";\n        } else if (target.classList.contains(\"transfo-scaler-bl\")) {\n            type = \"bl\";\n        } else if (target.classList.contains(\"transfo-scaler-tc\")) {\n            type = \"tc\";\n        } else if (target.classList.contains(\"transfo-scaler-bc\")) {\n            type = \"bc\";\n        } else if (target.classList.contains(\"transfo-scaler-ml\")) {\n            type = \"ml\";\n        } else if (target.classList.contains(\"transfo-scaler-mr\")) {\n            type = \"mr\";\n        }\n\n        this.transfo.active = {\n            type: type,\n            scalex: this.transfo.settings.scalex,\n            scaley: this.transfo.settings.scaley,\n            pageX: ev.pageX,\n            pageY: ev.pageY,\n            center: this.getOffset(this.transfoCenter.el),\n        };\n    }\n\n    computeImageTransformations() {\n        this.transfo = {};\n        const transform = this.image.style.transform || \"\";\n\n        this.transfo.settings = {};\n\n        this.transfo.settings.angle =\n            transform.indexOf(\"rotate\") != -1\n                ? parseFloat(transform.match(/rotate\\(([^)]+)deg\\)/)[1])\n                : 0;\n        this.transfo.settings.scalex =\n            transform.indexOf(\"scaleX\") != -1\n                ? parseFloat(transform.match(/scaleX\\(([^)]+)\\)/)[1])\n                : 1;\n        this.transfo.settings.scaley =\n            transform.indexOf(\"scaleY\") != -1\n                ? parseFloat(transform.match(/scaleY\\(([^)]+)\\)/)[1])\n                : 1;\n\n        this.image.style.transform = \"\";\n\n        this.transfo.settings.pos = this.getOffset(this.image);\n\n        this.transfo.settings.height = this.image.clientHeight;\n        this.transfo.settings.width = this.image.clientWidth;\n\n        const translatex = transform.match(/translateX\\(([0-9.-]+)(%|px)\\)/);\n        const translatey = transform.match(/translateY\\(([0-9.-]+)(%|px)\\)/);\n        this.transfo.settings.translate = \"%\";\n\n        if (translatex && translatex[2] === \"%\") {\n            this.transfo.settings.translatexp = parseFloat(translatex[1]);\n            this.transfo.settings.translatex =\n                (this.transfo.settings.translatexp / 100) * this.transfo.settings.width;\n        } else {\n            this.transfo.settings.translatex = translatex ? parseFloat(translatex[1]) : 0;\n        }\n        if (translatey && translatey[2] === \"%\") {\n            this.transfo.settings.translateyp = parseFloat(translatey[1]);\n            this.transfo.settings.translatey =\n                (this.transfo.settings.translateyp / 100) * this.transfo.settings.height;\n        } else {\n            this.transfo.settings.translatey = translatey ? parseFloat(translatey[1]) : 0;\n        }\n\n        this.transfo.settings.css = window.getComputedStyle(this.image, null);\n        this.transfo.settings.rotationStep = 5;\n    }\n\n    positionTransfoContainer() {\n        const settings = this.transfo.settings;\n        const width = parseFloat(settings.css.width);\n        const height = parseFloat(settings.css.height);\n        settings.translatexp = Math.round((settings.translatex / width) * 1000) / 10;\n        settings.translateyp = Math.round((settings.translatey / height) * 1000) / 10;\n\n        this.setImageTransformation(this.image);\n\n        this.transfoContainer.el.style.position = \"absolute\";\n        this.transfoContainer.el.style.width = width + \"px\";\n        this.transfoContainer.el.style.height = height + \"px\";\n        this.transfoContainer.el.style.top = settings.pos.top + \"px\";\n        this.transfoContainer.el.style.left = settings.pos.left + \"px\";\n\n        const controls = this.transfoControls.el;\n\n        this.setImageTransformation(controls);\n        controls.style.width = width + \"px\";\n        controls.style.height = height + \"px\";\n        controls.style.cursor = \"move\";\n\n        for (const child of controls.children) {\n            child.style.transform =\n                \"scaleX(\" + 1 / settings.scalex + \") scaleY(\" + 1 / settings.scaley + \")\";\n        }\n    }\n\n    setImageTransformation(element) {\n        let transform = \"\";\n        if (this.transfo.settings.angle !== 0) {\n            transform += \" rotate(\" + this.transfo.settings.angle + \"deg) \";\n        }\n        if (this.transfo.settings.translatex) {\n            transform +=\n                \" translateX(\" +\n                (this.transfo.settings.translate === \"%\"\n                    ? this.transfo.settings.translatexp + \"%\"\n                    : this.transfo.settings.translatex + \"px\") +\n                \") \";\n        }\n        if (this.transfo.settings.translatey) {\n            transform +=\n                \" translateY(\" +\n                (this.transfo.settings.translate === \"%\"\n                    ? this.transfo.settings.translateyp + \"%\"\n                    : this.transfo.settings.translatey + \"px\") +\n                \") \";\n        }\n        if (this.transfo.settings.scalex != 1) {\n            transform += \" scaleX(\" + this.transfo.settings.scalex + \") \";\n        }\n        if (this.transfo.settings.scaley != 1) {\n            transform += \" scaleY(\" + this.transfo.settings.scaley + \") \";\n        }\n        element.style.transform = transform;\n    }\n\n    getOffset(target) {\n        if (!target.getClientRects().length) {\n            return { top: 0, left: 0 };\n        } else {\n            const rect = target.getBoundingClientRect();\n            const win = target.ownerDocument.defaultView;\n            const frameElement = target.ownerDocument.defaultView.frameElement;\n            const offset = { top: 0, left: 0 };\n            if (frameElement) {\n                const frameRect = frameElement.getBoundingClientRect();\n                offset.left += frameRect.left;\n                offset.top += frameRect.top;\n            }\n            return {\n                top: rect.top + win.pageYOffset + offset.top,\n                left: rect.left + win.pageXOffset + offset.left,\n            };\n        }\n    }\n\n    resetHandlers() {\n        this.computeImageTransformations();\n        this.positionTransfoContainer();\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Attachment, FileSelector, IMAGE_MIMETYPES } from \"./file_selector\";\nimport { renderToElement } from \"@web/core/utils/render\";\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                let url = `/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                    url += `&access_token=${encodeURIComponent(accessToken)}`;\n                }\n                return this.renderFileElement(attachment, url);\n            })\n        );\n    }\n\n    static renderFileElement(attachment, downloadUrl) {\n        return renderStaticFileBox(attachment.name, attachment.mimetype, downloadUrl);\n    }\n}\n\nexport function renderStaticFileBox(filename, mimetype, downloadUrl) {\n    const rootSpan = document.createElement(\"span\");\n    rootSpan.classList.add(\"o_file_box\");\n    rootSpan.contentEditable = false;\n    const bannerElement = renderToElement(\"html_editor.StaticFileBox\", {\n        fileModel: { filename, mimetype, downloadUrl },\n    });\n    rootSpan.append(bannerElement);\n    return rootSpan;\n}\n", "/**\n * This file is no longer used, and is kept for compatibility (stable policy).\n * To be removed in master.\n */\n\nimport { 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", "/**\n * This file is no longer used, and is kept for compatibility (stable policy).\n * To be removed in master.\n */\n\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { MediaDialog } from \"@html_editor/main/media/media_dialog/media_dialog\";\nimport { FileDocumentsSelector } from \"./file_documents_selector\";\nimport { _t } from \"@web/core/l10n/translation\";\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            id: \"MIXED_FILES\",\n            title: _t(\"Documents\"),\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.toLowerCase()) >= 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 { 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        sequence: 10,\n    },\n    ICONS: {\n        id: \"ICONS\",\n        title: _t(\"Icons\"),\n        Component: IconSelector,\n        sequence: 20,\n    },\n    VIDEOS: {\n        id: \"VIDEOS\",\n        title: _t(\"Videos\"),\n        Component: VideoSelector,\n        sequence: 30,\n    },\n};\n\nconst DEFAULT_SEQUENCE = 50;\nconst sequence = (tab) => tab.sequence ?? DEFAULT_SEQUENCE;\n\nexport class MediaDialog extends Component {\n    static template = \"html_editor.MediaDialog\";\n    static defaultProps = {\n        useMediaLibrary: true,\n    };\n    static components = {\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.selectedMedia = useState({});\n\n        this.addButtonRef = useRef(\"add-button\");\n\n        this.initialIconClasses = [];\n\n        this.tabs = { ...TABS };\n\n        this.notebookPages = [];\n        this.addDefaultTabs();\n        this.addExtraTabs();\n        this.notebookPages.sort((a, b) => sequence(a) - sequence(b));\n\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(this.tabs).find((id) =>\n                this.tabs[id].Component.tagNames.includes(this.props.media.tagName)\n            );\n            if (correspondingTab) {\n                return correspondingTab;\n            }\n        }\n        return this.notebookPages[0].id;\n    }\n\n    addTab(tab, additionalProps = {}) {\n        this.selectedMedia[tab.id] = [];\n        this.notebookPages.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    /**\n     * Method no longer used, kept for compatibility (stable policy).\n     * To be removed in master.\n     */\n    addTabs() {\n        this.addDefaultTabs();\n    }\n\n    addDefaultTabs() {\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 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 (!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    addExtraTabs() {\n        for (const tab of this.props.extraTabs || []) {\n            this.addTab(tab);\n            this.tabs[tab.id] = tab;\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 this.tabs[this.state.activeTab].Component.createElements(\n            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 === this.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(this.tabs).filter(\n                (key) => key !== this.state.activeTab\n            )) {\n                for (const property of this.tabs[otherTab].Component.mediaSpecificStyles) {\n                    element.style.removeProperty(property);\n                }\n                element.classList.remove(...this.tabs[otherTab].Component.mediaSpecificClasses);\n                const extraClassesToRemove = [];\n                for (const name of this.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 this.tabs[this.state.activeTab].Component\n                            .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(\n                ...this.tabs[this.state.activeTab].Component.mediaSpecificClasses\n            );\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 !== this.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 { Plugin } from \"@html_editor/plugin\";\nimport {\n    ICON_SELECTOR,\n    isIconElement,\n    isProtected,\n    isProtecting,\n} from \"@html_editor/utils/dom_info\";\nimport { backgroundImageCssToParts, backgroundImagePartsToCss } from \"@html_editor/utils/image\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { MediaDialog } from \"./media_dialog/media_dialog\";\nimport { rightPos } from \"@html_editor/utils/position\";\nimport { withSequence } from \"@html_editor/utils/resource\";\n\nconst MEDIA_SELECTOR = `${ICON_SELECTOR} , .o_image, .media_iframe_video`;\n\n/**\n * @typedef { Object } MediaShared\n * @property { MediaPlugin['savePendingImages'] } savePendingImages\n */\n\nexport class MediaPlugin extends Plugin {\n    static id = \"media\";\n    static dependencies = [\"selection\", \"history\", \"dom\", \"dialog\"];\n    static shared = [\"savePendingImages\"];\n    resources = {\n        user_commands: [\n            {\n                id: \"replaceImage\",\n                title: _t(\"Replace media\"),\n                run: this.replaceImage.bind(this),\n            },\n            {\n                id: \"insertMedia\",\n                title: _t(\"Media\"),\n                description: _t(\"Insert image or icon\"),\n                keywords: [_t(\"Image\"), _t(\"Icon\")],\n                icon: \"fa-file-image-o\",\n                run: this.openMediaDialog.bind(this),\n            },\n        ],\n        toolbar_groups: withSequence(29, {\n            id: \"replace_image\",\n            namespace: \"image\",\n        }),\n        toolbar_items: [\n            {\n                id: \"replace_image\",\n                groupId: \"replace_image\",\n                commandId: \"replaceImage\",\n                text: \"Replace\",\n            },\n        ],\n        powerbox_categories: withSequence(40, { id: \"media\", name: _t(\"Media\") }),\n        powerbox_items: [\n            ...(this.config.disableImage\n                ? []\n                : [{ categoryId: \"media\", commandId: \"insertMedia\" }]),\n        ],\n        power_buttons: withSequence(1, { commandId: \"insertMedia\" }),\n\n        /** Handlers */\n        clean_handlers: this.clean.bind(this),\n        clean_for_save_handlers: ({ root }) => this.cleanForSave(root),\n        normalize_handlers: this.normalizeMedia.bind(this),\n\n        unsplittable_node_predicates: isIconElement, // avoid merge\n    };\n\n    get recordInfo() {\n        return this.config.getRecordInfo ? this.config.getRecordInfo() : {};\n    }\n\n    replaceImage() {\n        const selectedNodes = this.dependencies.selection.getSelectedNodes();\n        const node = selectedNodes.find((node) => node.tagName === \"IMG\");\n        if (node) {\n            this.openMediaDialog({ node });\n            this.dependencies.history.addStep();\n        }\n    }\n\n    normalizeMedia(node) {\n        const mediaElements = [...node.querySelectorAll(MEDIA_SELECTOR)];\n        if (node.matches(MEDIA_SELECTOR)) {\n            mediaElements.push(node);\n        }\n        for (const el of mediaElements) {\n            if (isProtected(el) || isProtecting(el)) {\n                continue;\n            }\n            el.setAttribute(\n                \"contenteditable\",\n                el.hasAttribute(\"contenteditable\") ? el.getAttribute(\"contenteditable\") : \"false\"\n            );\n            if (isIconElement(el)) {\n                el.textContent = \"\\u200B\";\n            }\n        }\n    }\n\n    clean(root) {\n        for (const el of root.querySelectorAll(MEDIA_SELECTOR)) {\n            if (isIconElement(el)) {\n                el.textContent = \"\";\n            }\n        }\n    }\n\n    cleanForSave(root) {\n        for (const el of root.querySelectorAll(MEDIA_SELECTOR)) {\n            if (isIconElement(el)) {\n                el.textContent = \"\";\n            }\n            el.removeAttribute(\"contenteditable\");\n        }\n    }\n\n    onSaveMediaDialog(element, { node }) {\n        if (!element) {\n            // @todo @phoenix to remove\n            throw new Error(\"Element is required: onSaveMediaDialog\");\n            // return;\n        }\n\n        if (node) {\n            const changedIcon = isIconElement(node) && isIconElement(element);\n            if (changedIcon) {\n                // Preserve tag name when changing an icon and not recreate the\n                // editors unnecessarily.\n                for (const attribute of element.attributes) {\n                    node.setAttribute(attribute.nodeName, attribute.nodeValue);\n                }\n            } else {\n                node.replaceWith(element);\n            }\n        } else {\n            this.dependencies.dom.insert(element);\n        }\n        // Collapse selection after the inserted/replaced element.\n        const [anchorNode, anchorOffset] = rightPos(element);\n        this.dependencies.selection.setSelection({ anchorNode, anchorOffset });\n        this.dependencies.history.addStep();\n    }\n\n    openMediaDialog(params = {}) {\n        const { resModel, resId, field, type } = this.recordInfo;\n        const mediaDialogClosedPromise = this.dependencies.dialog.addDialog(MediaDialog, {\n            resModel,\n            resId,\n            useMediaLibrary: !!(\n                field &&\n                ((resModel === \"ir.ui.view\" && field === \"arch\") || type === \"html\")\n            ), // @todo @phoenix: should be removed and moved to config.mediaModalParams\n            media: params.node,\n            save: (element) => {\n                this.onSaveMediaDialog(element, { node: params.node });\n            },\n            onAttachmentChange: this.config.onAttachmentChange || (() => {}),\n            noVideos: !!this.config.disableVideo,\n            noImages: !!this.config.disableImage,\n            extraTabs: this.getResource(\"media_dialog_extra_tabs\"),\n            ...this.config.mediaModalParams,\n            ...params,\n        });\n        return mediaDialogClosedPromise;\n    }\n\n    async savePendingImages() {\n        const editableEl = this.editable;\n        const { resModel, resId } = this.recordInfo;\n        // When saving a webp, o_b64_image_to_save is turned into\n        // o_modified_image_to_save by saveB64Image to request the saving\n        // of the pre-converted webp resizes and all the equivalent jpgs.\n        const b64Proms = [...editableEl.querySelectorAll(\".o_b64_image_to_save\")].map(\n            async (el) => {\n                const dirtyEditable = el.closest(\".o_dirty\");\n                if (dirtyEditable && dirtyEditable !== editableEl) {\n                    // Do nothing as there is an editable element closer to the\n                    // image that will perform the `saveB64Image()` call with\n                    // the correct \"resModel\" and \"resId\" parameters.\n                    return;\n                }\n                await this.saveB64Image(el, resModel, resId);\n            }\n        );\n        const modifiedProms = [...editableEl.querySelectorAll(\".o_modified_image_to_save\")].map(\n            async (el) => {\n                const dirtyEditable = el.closest(\".o_dirty\");\n                if (dirtyEditable && dirtyEditable !== editableEl) {\n                    // Do nothing as there is an editable element closer to the\n                    // image that will perform the `saveModifiedImage()` call\n                    // with the correct \"resModel\" and \"resId\" parameters.\n                    return;\n                }\n                await this.saveModifiedImage(el, resModel, resId);\n            }\n        );\n        const proms = [...b64Proms, ...modifiedProms];\n        const hasChange = !!proms.length;\n        if (hasChange) {\n            await Promise.all(proms);\n        }\n        return hasChange;\n    }\n\n    createAttachment({ el, imageData, resModel, resId }) {\n        return rpc(\"/html_editor/attachment/add_data\", {\n            name: el.dataset.fileName || \"\",\n            data: imageData,\n            is_image: true,\n            res_model: resModel,\n            res_id: resId,\n        });\n    }\n\n    /**\n     * Saves a base64 encoded image as an attachment.\n     * Relies on saveModifiedImage being called after it for webp.\n     *\n     * @private\n     * @param {Element} el\n     * @param {string} resModel\n     * @param {number} resId\n     */\n    async saveB64Image(el, resModel, resId) {\n        const imageData = el.getAttribute(\"src\").split(\"base64,\")[1];\n        if (!imageData) {\n            // Checks if the image is in base64 format for RPC call. Relying\n            // only on the presence of the class \"o_b64_image_to_save\" is not\n            // robust enough.\n            el.classList.remove(\"o_b64_image_to_save\");\n            return;\n        }\n        const attachment = await this.createAttachment({\n            el,\n            imageData,\n            resId,\n            resModel,\n        });\n        if (!attachment) {\n            return;\n        }\n        if (attachment.mimetype === \"image/webp\") {\n            el.classList.add(\"o_modified_image_to_save\");\n            el.dataset.originalId = attachment.id;\n            el.dataset.mimetype = attachment.mimetype;\n            el.dataset.fileName = attachment.name;\n            return this.saveModifiedImage(el, resModel, resId);\n        } else {\n            let src = attachment.image_src;\n            if (!attachment.public) {\n                let accessToken = attachment.access_token;\n                if (!accessToken) {\n                    [accessToken] = await this.services.orm.call(\n                        \"ir.attachment\",\n                        \"generate_access_token\",\n                        [attachment.id]\n                    );\n                }\n                src += `?access_token=${encodeURIComponent(accessToken)}`;\n            }\n            el.setAttribute(\"src\", src);\n        }\n        el.classList.remove(\"o_b64_image_to_save\");\n    }\n\n    /**\n     * Saves a modified image as an attachment.\n     *\n     * @private\n     * @param {Element} el\n     * @param {string} resModel\n     * @param {number} resId\n     */\n    async saveModifiedImage(el, resModel, resId) {\n        const isBackground = !el.matches(\"img\");\n        // Modifying an image always creates a copy of the original, even if\n        // it was modified previously, as the other modified image may be used\n        // elsewhere if the snippet was duplicated or was saved as a custom one.\n        let altData = undefined;\n        const isImageField = !!el.closest(\"[data-oe-type=image]\");\n        if (el.dataset.mimetype === \"image/webp\" && isImageField) {\n            // Generate alternate sizes and format for reports.\n            altData = {};\n            const image = document.createElement(\"img\");\n            image.src = isBackground ? el.dataset.bgSrc : el.getAttribute(\"src\");\n            await new Promise((resolve) => image.addEventListener(\"load\", resolve));\n            const originalSize = Math.max(image.width, image.height);\n            const smallerSizes = [1024, 512, 256, 128].filter((size) => size < originalSize);\n            for (const size of [originalSize, ...smallerSizes]) {\n                const ratio = size / originalSize;\n                const canvas = document.createElement(\"canvas\");\n                canvas.width = image.width * ratio;\n                canvas.height = image.height * ratio;\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(\n                    image,\n                    0,\n                    0,\n                    image.width,\n                    image.height,\n                    0,\n                    0,\n                    canvas.width,\n                    canvas.height\n                );\n                altData[size] = {\n                    \"image/jpeg\": canvas.toDataURL(\"image/jpeg\", 0.75).split(\",\")[1],\n                };\n                if (size !== originalSize) {\n                    altData[size][\"image/webp\"] = canvas\n                        .toDataURL(\"image/webp\", 0.75)\n                        .split(\",\")[1];\n                }\n            }\n        }\n        const newAttachmentSrc = await rpc(\n            `/html_editor/modify_image/${encodeURIComponent(el.dataset.originalId)}`,\n            {\n                res_model: resModel,\n                res_id: parseInt(resId),\n                data: (isBackground ? el.dataset.bgSrc : el.getAttribute(\"src\")).split(\",\")[1],\n                alt_data: altData,\n                mimetype: isBackground\n                    ? el.dataset.mimetype\n                    : el.getAttribute(\"src\").split(\":\")[1].split(\";\")[0],\n                name: el.dataset.fileName ? el.dataset.fileName : null,\n            }\n        );\n        el.classList.remove(\"o_modified_image_to_save\");\n        if (isBackground) {\n            const parts = backgroundImageCssToParts(el.style[\"background-image\"]);\n            parts.url = `url('${newAttachmentSrc}')`;\n            const combined = backgroundImagePartsToCss(parts);\n            el.style[\"background-image\"] = combined;\n            delete el.dataset.bgSrc;\n        } else {\n            el.setAttribute(\"src\", newAttachmentSrc);\n        }\n    }\n}\n", "import { useNativeDraggable } from \"@html_editor/utils/drag_and_drop\";\nimport { endPos } from \"@html_editor/utils/position\";\nimport { Plugin } from \"../plugin\";\nimport { ancestors, closestElement } from \"../utils/dom_traversal\";\nimport { baseContainerGlobalSelector } from \"@html_editor/utils/base_container\";\n\nconst WIDGET_CONTAINER_WIDTH = 25;\nconst WIDGET_MOVE_SIZE = 20;\n\nconst ALLOWED_ELEMENTS =\n    \"h1, h2, h3, p, hr, pre, blockquote, ul, ol, table, [data-embedded], .o_text_columns, .o_editor_banner, .oe_movable\";\n\nexport class MoveNodePlugin extends Plugin {\n    static id = \"movenode\";\n    static dependencies = [\"baseContainer\", \"selection\", \"history\", \"position\", \"localOverlay\"];\n    resources = {\n        layout_geometry_change_handlers: () => {\n            if (this.currentMovableElement) {\n                this.setMovableElement(this.currentMovableElement);\n            }\n            this.updateHooks();\n        },\n    };\n\n    setup() {\n        this.intersectionObserver = new IntersectionObserver(\n            this.intersectionObserverCallback.bind(this),\n            {\n                root: document,\n            }\n        );\n        this.visibleMovableElements = new Set();\n\n        this.elementHookMap = new Map();\n\n        this.addDomListener(this.editable, \"mousemove\", this.onMousemove, true);\n        this.addDomListener(this.editable, \"touchmove\", this.onMousemove, true);\n        this.addDomListener(this.document, \"keydown\", this.onDocumentKeydown, true);\n        this.addDomListener(this.document, \"mousemove\", this.onDocumentMousemove, true);\n        this.addDomListener(this.document, \"touchmove\", this.onDocumentMousemove, true);\n\n        // This container help to add zone into which the mouse can activate the move widget.\n        this.widgetHookContainer = this.dependencies.localOverlay.makeLocalOverlay(\n            \"oe-widget-hooks-container\"\n        );\n        // This container contains the differents widgets.\n        this.widgetContainer =\n            this.dependencies.localOverlay.makeLocalOverlay(\"oe-widgets-container\");\n        // This container contains the jquery helper element.\n        this.dragHelperContainer = this.dependencies.localOverlay.makeLocalOverlay(\n            \"oe-movenode-helper-container\"\n        );\n        // This container contains drop zones. They are the zones that handle where the drop should happen.\n        this.dropzonesContainer =\n            this.dependencies.localOverlay.makeLocalOverlay(\"oe-dropzones-container\");\n        // This container contains drop hint. The final rectangle showed to the user.\n        this.dropzoneHintContainer = this.dependencies.localOverlay.makeLocalOverlay(\n            \"oe-dropzone-hint-container\"\n        );\n\n        // Uncomment line for debugging tranparent zones\n        // this.widgetHookContainer.classList.add(\"debug\");\n        // this.dropzonesContainer.classList.add(\"debug\");\n\n        this.scrollableElement = closestElement(this.editable.parentElement);\n        while (\n            this.scrollableElement &&\n            getComputedStyle(this.scrollableElement).overflowY !== \"auto\"\n        ) {\n            this.scrollableElement = this.scrollableElement.parentElement;\n        }\n        this.scrollableElement = this.scrollableElement || this.editable;\n\n        this.resetHooksNextMousemove = true;\n        this.mutationObserver = new MutationObserver(() => {\n            this.resetHooksNextMousemove = true;\n            this.removeMoveWidget();\n        });\n        this.mutationObserver.observe(this.editable, {\n            childList: true,\n            subtree: true,\n            characterData: true,\n            characterDataOldValue: true,\n        });\n    }\n    destroy() {\n        super.destroy();\n        this.intersectionObserver.disconnect();\n        this.mutationObserver.disconnect();\n        this.smoothScrollOnDrag && this.smoothScrollOnDrag.destroy();\n    }\n    intersectionObserverCallback(entries) {\n        for (const entry of entries) {\n            const element = entry.target;\n            if (entry.isIntersecting) {\n                this.visibleMovableElements.add(element);\n                this.resetHooksNextMousemove = true;\n            } else {\n                this.visibleMovableElements.delete(element);\n                const hookElement = this.elementHookMap.get(element);\n                if (hookElement) {\n                    // If hookElement is undefined, it means that this callback\n                    // was called after a new element was inserted in the\n                    // editable, but before the next updateHooks. The hook will\n                    // be created when that happens.\n                    hookElement.style.display = `none`;\n                }\n            }\n        }\n    }\n    updateHooks() {\n        const editableStyles = getComputedStyle(this.editable);\n        this.editableRect = this.editable.getBoundingClientRect();\n        const paddingLeft = parseInt(editableStyles.paddingLeft, 10) || 0;\n        this.editableRect.x = this.editableRect.x + paddingLeft - (WIDGET_CONTAINER_WIDTH + 5);\n        this.editableRect.width =\n            this.editableRect.width - paddingLeft + (WIDGET_CONTAINER_WIDTH + 5);\n        const containerRect = this.widgetHookContainer.getBoundingClientRect();\n        const elements = this.getMovableElements();\n\n        const elementsToGarbageCollect = new Set(this.elementHookMap.keys());\n        for (const index in elements) {\n            const element = elements[index];\n            elementsToGarbageCollect.delete(element);\n            let hookElement = this.elementHookMap.get(element);\n            if (!hookElement) {\n                hookElement = document.createElement(\"div\");\n                this.elementHookMap.set(element, hookElement);\n                hookElement.classList.add(\"oe-dropzone-hook\");\n                hookElement.addEventListener(\"mouseenter\", () => {\n                    if (element !== this.currentMovableElement) {\n                        this.setMovableElement(element);\n                    }\n                });\n                this.widgetHookContainer.append(hookElement);\n                hookElement.style.display = `none`;\n\n                this.intersectionObserver.observe(element);\n            }\n            hookElement.style.zIndex = index;\n        }\n        // For all the elements that are not in the dom, remove their\n        // corresponding hook.\n        for (const element of elementsToGarbageCollect) {\n            this.visibleMovableElements.delete(element);\n            this.elementHookMap.get(element).remove();\n            this.intersectionObserver.unobserve(element);\n            this.elementHookMap.delete(element);\n        }\n\n        const visibleElements = [...this.visibleMovableElements];\n        // Prevent layout thrashing by computing all the rects in advance.\n        const elementRects = visibleElements.map((element) => element.getBoundingClientRect());\n        for (const index in visibleElements) {\n            const element = visibleElements[index];\n            const elementRect = elementRects[index];\n            const hookElement = this.elementHookMap.get(element);\n\n            const style = getComputedStyle(element);\n            const marginTop = parseInt(style.marginTop, 10) || 0;\n            const marginBottom = parseInt(style.marginBottom, 10) || 0;\n            let hookBox;\n            if (element.tagName === \"HR\") {\n                hookBox = new DOMRect(\n                    elementRect.x - containerRect.left - WIDGET_CONTAINER_WIDTH,\n                    elementRect.y - containerRect.top - marginTop,\n                    elementRect.width + WIDGET_CONTAINER_WIDTH,\n                    elementRect.height + marginTop + marginBottom\n                );\n            } else {\n                hookBox = new DOMRect(\n                    elementRect.x - containerRect.left - WIDGET_CONTAINER_WIDTH,\n                    elementRect.y - containerRect.top - marginTop,\n                    WIDGET_CONTAINER_WIDTH,\n                    elementRect.height + marginTop + marginBottom\n                );\n            }\n\n            hookElement.style.left = `${hookBox.x}px`;\n            hookElement.style.top = `${hookBox.y}px`;\n            hookElement.style.width = `${hookBox.width}px`;\n            hookElement.style.height = `${hookBox.height}px`;\n            hookElement.style.display = `block`;\n        }\n    }\n    _updateAnchorWidgets(newAnchorWidget) {\n        let movableElement =\n            newAnchorWidget &&\n            closestElement(newAnchorWidget, (node) => {\n                return (\n                    isNodeMovable(node) &&\n                    node.matches([ALLOWED_ELEMENTS, baseContainerGlobalSelector].join(\", \"))\n                );\n            });\n        // Retrive the first list container from the ancestors.\n        const listContainer =\n            movableElement &&\n            ancestors(movableElement, this.editable)\n                .reverse()\n                .find((n) => [\"UL\", \"OL\"].includes(n.tagName));\n        movableElement = listContainer || movableElement;\n        if (movableElement && movableElement !== this.currentMovableElement) {\n            this.setMovableElement(movableElement);\n        }\n    }\n    getMovableElements() {\n        const elems = [];\n        for (const el of this.editable.querySelectorAll(\n            [ALLOWED_ELEMENTS, baseContainerGlobalSelector].join(\", \")\n        )) {\n            if (isNodeMovable(el)) {\n                elems.push(el);\n            }\n        }\n        return elems;\n    }\n    getDroppableElements(draggableNode) {\n        return this.getMovableElements().filter(\n            (node) => !closestElement(node.parentElement, (n) => n === draggableNode)\n        );\n    }\n    setMovableElement(movableElement) {\n        this.removeMoveWidget();\n        this.currentMovableElement = movableElement;\n        this.dispatchTo(\"set_movable_element_handlers\", movableElement);\n\n        const containerRect = this.widgetContainer.getBoundingClientRect();\n        const anchorBlockRect = this.currentMovableElement.getBoundingClientRect();\n        const closestList = closestElement(this.currentMovableElement, \"ul, ol\"); // Prevent overlap bullets.\n        const anchorX = closestList ? closestList.getBoundingClientRect().x : anchorBlockRect.x;\n        let anchorY = anchorBlockRect.y;\n        if (this.currentMovableElement.tagName.match(/H[1-6]/)) {\n            anchorY += (anchorBlockRect.height - WIDGET_MOVE_SIZE) / 2;\n        }\n\n        this.moveWidget = this.document.createElement(\"div\");\n        this.moveWidget.className = \"oe-sidewidget-move fa fa-sort\";\n        this.widgetContainer.append(this.moveWidget);\n\n        let moveWidgetOffsetTop = 0;\n        if (movableElement.tagName === \"HR\") {\n            const style = getComputedStyle(movableElement);\n            moveWidgetOffsetTop = parseInt(style.marginTop, 10) || 0;\n        }\n\n        this.moveWidget.style.width = `${WIDGET_MOVE_SIZE}px`;\n        this.moveWidget.style.height = `${WIDGET_MOVE_SIZE}px`;\n        this.moveWidget.style.top = `${anchorY - containerRect.y - moveWidgetOffsetTop}px`;\n        this.moveWidget.style.left = `${anchorX - containerRect.x - WIDGET_CONTAINER_WIDTH}px`;\n\n        if (this.scrollableElement) {\n            this.smoothScrollOnDrag && this.smoothScrollOnDrag.destroy();\n            // TODO: This should be made more generic, one hook for the entire\n            // editable with each element handled.\n            this.smoothScrollOnDrag = useNativeDraggable(simpleDraggableHook, {\n                ref: { el: this.widgetContainer },\n                elements: \".oe-sidewidget-move\",\n                onDragStart: () => this.startDropzones(movableElement, containerRect),\n                onDragEnd: () => this._stopDropzones(movableElement),\n                helper: () => {\n                    const container = document.createElement(\"div\");\n                    container.append(movableElement.cloneNode(true));\n                    const style = getComputedStyle(movableElement);\n                    container.style.height = style.height;\n                    container.style.width = style.width;\n                    container.style.paddingLeft = \"25px\";\n                    container.style.opacity = \"0.4\";\n                    this.dragHelperContainer.append(container);\n                    return container;\n                },\n            });\n        }\n    }\n    removeMoveWidget() {\n        this.dispatchTo(\"unset_movable_element_handlers\");\n        this.moveWidget?.remove();\n        this.moveWidget = undefined;\n        this.currentMovableElement = undefined;\n    }\n    startDropzones(movableElement, containerRect, directions = [\"north\", \"south\"]) {\n        this.removeMoveWidget();\n        const elements = this.getDroppableElements(movableElement);\n\n        this.dropzonesContainer.replaceChildren();\n        this.editable.classList.add(\"oe-editor-dragging\");\n\n        for (const element of elements) {\n            const originalRect = element.getBoundingClientRect();\n            const style = getComputedStyle(element);\n            const marginTop = parseInt(style.marginTop, 10);\n            const marginBottom = parseInt(style.marginBottom, 10);\n            const marginLeft = parseInt(style.marginLeft, 10);\n            const marginRight = parseInt(style.marginRight, 10);\n\n            const dropzoneRect = new DOMRect(\n                originalRect.left - marginLeft - WIDGET_CONTAINER_WIDTH,\n                originalRect.top - marginTop,\n                originalRect.width + marginLeft + marginRight + WIDGET_CONTAINER_WIDTH,\n                originalRect.height + marginTop + marginBottom\n            );\n            const dropzoneHintRect = new DOMRect(\n                originalRect.left - marginLeft,\n                originalRect.top - marginTop,\n                originalRect.width + marginLeft + marginRight,\n                originalRect.height + marginTop + marginBottom\n            );\n\n            const dropzoneBox = document.createElement(\"div\");\n            dropzoneBox.className = `oe-dropzone-box`;\n            dropzoneBox.style.top = `${dropzoneRect.top - containerRect.top}px`;\n            dropzoneBox.style.left = `${dropzoneRect.left - containerRect.left}px`;\n            dropzoneBox.style.width = `${dropzoneRect.width}px`;\n            dropzoneBox.style.height = `${dropzoneRect.height}px`;\n\n            const dropzoneHintBox = document.createElement(\"div\");\n            dropzoneHintBox.className = `oe-dropzone-box`;\n            dropzoneHintBox.style.top = `${dropzoneHintRect.top - containerRect.top}px`;\n            dropzoneHintBox.style.left = `${dropzoneHintRect.left - containerRect.left}px`;\n            dropzoneHintBox.style.width = `${dropzoneHintRect.width}px`;\n            dropzoneHintBox.style.height = `${dropzoneHintRect.height}px`;\n\n            const sideElements = {};\n            for (const direction of directions) {\n                const sideElement = document.createElement(\"div\");\n                sideElement.className = `oe-dropzone-box-side oe-dropzone-box-side-${direction}`;\n                sideElements[direction] = sideElement;\n                dropzoneBox.append(sideElement);\n                const onEnter = () => {\n                    this._currentZone = [direction];\n\n                    removeDropHint();\n                    this._currentDropHint = document.createElement(\"div\");\n                    this._currentDropHint.className = `oe-current-drop-hint`;\n                    const currentDropHintSize = 4;\n                    const currentDropHintSizeHalf = currentDropHintSize / 2;\n\n                    if (direction === \"north\") {\n                        this._currentDropHint.style[\"top\"] = `-${currentDropHintSizeHalf}px`;\n                        this._currentDropHint.style[\"width\"] = `100%`;\n                        this._currentDropHint.style[\"height\"] = `${currentDropHintSize}px`;\n                        dropzoneHintBox.append(this._currentDropHint);\n                        this._currentDropHintElementPosition = [\"top\", element];\n                    } else if (direction === \"south\") {\n                        this._currentDropHint.style[\"bottom\"] = `-${currentDropHintSizeHalf}px`;\n                        this._currentDropHint.style[\"width\"] = `100%`;\n                        this._currentDropHint.style[\"height\"] = `${currentDropHintSize}px`;\n                        dropzoneHintBox.append(this._currentDropHint);\n                        this._currentDropHintElementPosition = [\"bottom\", element];\n                    } else if (direction === \"west\") {\n                        this._currentDropHint.style[\"left\"] = `-${currentDropHintSizeHalf}px`;\n                        this._currentDropHint.style[\"height\"] = `100%`;\n                        this._currentDropHint.style[\"width\"] = `${currentDropHintSize}px`;\n                        dropzoneHintBox.append(this._currentDropHint);\n                        this._currentDropHintElementPosition = [\"left\", element];\n                    } else if (direction === \"east\") {\n                        this._currentDropHint.style[\"right\"] = `-${currentDropHintSizeHalf}px`;\n                        this._currentDropHint.style[\"height\"] = `100%`;\n                        this._currentDropHint.style[\"width\"] = `${currentDropHintSize}px`;\n                        dropzoneHintBox.append(this._currentDropHint);\n                        this._currentDropHintElementPosition = [\"right\", element];\n                    }\n                };\n                sideElement.addEventListener(\"mouseenter\", onEnter);\n                sideElement.addEventListener(\"pointerenter\", onEnter);\n                const removeDropHint = () => {\n                    if (this._currentDropHint) {\n                        this._currentDropHint.remove();\n                        this._currentDropHint = null;\n                    }\n                    this._currentDropHintElementPosition = null;\n                };\n                dropzoneBox.addEventListener(\"mouseleave\", removeDropHint);\n                dropzoneBox.addEventListener(\"pointerleave\", removeDropHint);\n            }\n\n            this.dropzonesContainer.append(dropzoneBox);\n            this.dropzoneHintContainer.append(dropzoneHintBox);\n        }\n    }\n    _stopDropzones(movableElement) {\n        this.editable.classList.remove(\"oe-editor-dragging\");\n        this.dropzonesContainer.replaceChildren();\n        this.dropzoneHintContainer.replaceChildren();\n\n        if (this._currentDropHintElementPosition) {\n            const [position, focusElelement] = this._currentDropHintElementPosition;\n            this._currentDropHintElementPosition = undefined;\n            const previousParent = movableElement.parentElement;\n            if (position === \"top\") {\n                focusElelement.before(movableElement);\n            } else if (position === \"bottom\") {\n                focusElelement.after(movableElement);\n            }\n            if (previousParent.innerHTML.trim() === \"\") {\n                const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n                const br = document.createElement(\"br\");\n                baseContainer.append(br);\n                previousParent.append(baseContainer);\n            }\n            const selectionPosition = endPos(movableElement);\n            this.dependencies.selection.setSelection({\n                anchorNode: selectionPosition[0],\n                anchorOffset: selectionPosition[1],\n            });\n            this.dependencies.history.addStep();\n        }\n    }\n    onMousemove(e) {\n        this._updateAnchorWidgets(e.target);\n    }\n    onDocumentKeydown() {\n        // Hide the move widget upon keystroke for visual clarity and provide\n        // visibility to a collaborative avatar.\n        this.removeMoveWidget();\n    }\n    onDocumentMousemove(e) {\n        if (this.resetHooksNextMousemove) {\n            this.resetHooksNextMousemove = false;\n            this.removeMoveWidget();\n            this.updateHooks();\n        }\n        const clientX = e.clientX ?? e.touches?.[0]?.clientX;\n        const clientY = e.clientY ?? e.touches?.[0]?.clientY;\n        if (this.editableRect && !isPointInside(this.editableRect, clientX, clientY)) {\n            this.removeMoveWidget();\n        }\n    }\n}\n\nfunction isNodeMovable(node) {\n    return (\n        node.parentElement?.getAttribute(\"contentEditable\") === \"true\" &&\n        !node.parentElement.closest(\".o_editor_banner\")\n    );\n}\n\nfunction isPointInside(rect, x, y) {\n    return rect.left <= x && rect.right >= x && rect.top <= y && rect.bottom >= y;\n}\n\nconst simpleDraggableHook = {\n    acceptedParams: {\n        helper: [Function],\n    },\n    edgeScrolling: { enable: true },\n    onComputeParams({ ctx, params }) {\n        ctx.helper = params.helper;\n        ctx.followCursor = false;\n        ctx.tolerance = 0;\n    },\n    onDragStart({ ctx }) {\n        ctx.current.element = ctx.helper();\n        ctx.current.element.style.left = `${ctx.pointer.x + 10}px`;\n        ctx.current.element.style.top = `${ctx.pointer.y + 10}px`;\n        ctx.current.element.style.position = \"fixed\";\n        // makeDraggableHook disables pointer events, we want them in this case\n        document.body.classList.remove(\"pe-none\");\n        return ctx.current;\n    },\n    onDrag({ ctx }) {\n        ctx.current.element.style.left = `${ctx.pointer.x}px`;\n        ctx.current.element.style.top = `${ctx.pointer.y}px`;\n    },\n    onDragEnd({ ctx }) {\n        ctx.current.element.remove();\n        return ctx.current;\n    },\n};\n", "import { ancestors } from \"@html_editor/utils/dom_traversal\";\nimport { Plugin } from \"../plugin\";\nimport { throttleForAnimation } from \"@web/core/utils/timing\";\nimport { couldBeScrollableX, couldBeScrollableY } from \"@web/core/utils/scrolling\";\n\n/**\n * This plugins provides a way to create a \"local\" overlays so that their\n * visibility is relative to the overflow of their ancestors.\n */\nexport class PositionPlugin extends Plugin {\n    static id = \"position\";\n    resources = {\n        // todo: it is strange that the position plugin is aware of external_history_step_handlers and history_reset_from_steps_handlers.\n        external_history_step_handlers: this.layoutGeometryChange.bind(this),\n        history_reset_from_steps_handlers: this.layoutGeometryChange.bind(this),\n        step_added_handlers: this.layoutGeometryChange.bind(this),\n    };\n\n    setup() {\n        this.layoutGeometryChange = throttleForAnimation(this.layoutGeometryChange.bind(this));\n        this.resizeObserver = new ResizeObserver(this.layoutGeometryChange);\n        this.resizeObserver.observe(this.document.body);\n        this.resizeObserver.observe(this.editable);\n        this.addDomListener(window, \"resize\", this.layoutGeometryChange);\n        if (this.document.defaultView !== window) {\n            this.addDomListener(this.document.defaultView, \"resize\", this.layoutGeometryChange);\n        }\n        const scrollableElements = [this.editable, ...ancestors(this.editable)].filter((node) => {\n            return couldBeScrollableX(node) || couldBeScrollableY(node);\n        });\n        for (const scrollableElement of scrollableElements) {\n            this.addDomListener(scrollableElement, \"scroll\", () => {\n                this.layoutGeometryChange();\n            });\n            this.resizeObserver.observe(scrollableElement);\n        }\n    }\n\n    destroy() {\n        this.resizeObserver.disconnect();\n        super.destroy();\n    }\n    layoutGeometryChange() {\n        this.dispatchTo(\"layout_geometry_change_handlers\");\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { baseContainerGlobalSelector } from \"@html_editor/utils/base_container\";\nimport { closestBlock } from \"@html_editor/utils/blocks\";\nimport { isEmptyBlock } from \"@html_editor/utils/dom_info\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { omit, pick } from \"@web/core/utils/objects\";\n\n/** @typedef {import(\"./powerbox/powerbox_plugin\").PowerboxCommand} PowerboxCommand */\n\n/**\n * @typedef {Object} PowerButton\n * @property {string} commandId\n * @property {Object} [commandParams]\n * @property {string} [title] Can be inferred from the user command\n * @property {string} [icon] Can be inferred from the user command\n * @property {string} [isAvailable] Can be inferred from the user command\n */\n/**\n * A power button is added by referencing an existing user command.\n *\n * Example:\n *\n * resources = {\n *      user_commands: [\n *          {\n *              id: myCommand,\n *              run: myCommandFunction,\n *              title: _t(\"My Command\"),\n *              icon: \"fa-bug\",\n *          },\n *      ],\n *      power_buttons: [\n *          {\n *              commandId: \"myCommand\",\n *              commandParams: { myParam: \"myValue\" },\n *              title: _t(\"My Power Button\"), // overrides the user command's `title`\n *              // `icon` is derived from the user command\n *          }\n *      ],\n * };\n */\n\nexport class PowerButtonsPlugin extends Plugin {\n    static id = \"powerButtons\";\n    static dependencies = [\n        \"baseContainer\",\n        \"selection\",\n        \"position\",\n        \"localOverlay\",\n        \"powerbox\",\n        \"userCommand\",\n    ];\n    resources = {\n        layout_geometry_change_handlers: this.updatePowerButtons.bind(this),\n        selectionchange_handlers: this.updatePowerButtons.bind(this),\n    };\n\n    setup() {\n        this.powerButtonsOverlay = this.dependencies.localOverlay.makeLocalOverlay(\n            \"oe-power-buttons-overlay\"\n        );\n        this.createPowerButtons();\n    }\n\n    createPowerButtons() {\n        const composePowerButton = (/**@type {PowerButton} */ item) => {\n            const command = this.dependencies.userCommand.getCommand(item.commandId);\n            return {\n                ...pick(command, \"title\", \"icon\", \"isAvailable\"),\n                ...omit(item, \"commandId\", \"commandParams\"),\n                run: () => command.run(item.commandParams),\n            };\n        };\n        const renderButton = ({ title, icon, run }) => {\n            const btn = this.document.createElement(\"button\");\n            btn.className = `power_button btn px-2 py-1 cursor-pointer fa ${icon}`;\n            btn.title = title;\n            this.addDomListener(btn, \"click\", () => this.applyCommand(run));\n            return btn;\n        };\n\n        /** @type {PowerButton[]} */\n        const powerButtonsDefinitions = this.getResource(\"power_buttons\");\n        // Merge properties from power_button and user_command.\n        const powerButtons = powerButtonsDefinitions.map(composePowerButton);\n        // Render HTML buttons.\n        this.descriptionToElementMap = new Map(powerButtons.map((pb) => [pb, renderButton(pb)]));\n\n        this.powerButtonsContainer = this.document.createElement(\"div\");\n        this.powerButtonsContainer.className = `o_we_power_buttons d-flex justify-content-center d-none`;\n        this.powerButtonsContainer.append(...this.descriptionToElementMap.values());\n        this.powerButtonsOverlay.append(this.powerButtonsContainer);\n    }\n\n    updatePowerButtons() {\n        this.powerButtonsContainer.classList.add(\"d-none\");\n        const { editableSelection, documentSelectionIsInEditable } =\n            this.dependencies.selection.getSelectionData();\n        if (!documentSelectionIsInEditable) {\n            return;\n        }\n        const block = closestBlock(editableSelection.anchorNode);\n        const element = closestElement(editableSelection.anchorNode);\n        if (\n            editableSelection.isCollapsed &&\n            element?.matches(baseContainerGlobalSelector) &&\n            isEmptyBlock(block) &&\n            !this.services.ui.isSmall &&\n            !closestElement(editableSelection.anchorNode, \"td\") &&\n            !block.style.textAlign &&\n            this.getResource(\"power_buttons_visibility_predicates\").every((predicate) =>\n                predicate(editableSelection)\n            )\n        ) {\n            this.powerButtonsContainer.classList.remove(\"d-none\");\n            const direction = closestElement(element, \"[dir]\")?.getAttribute(\"dir\");\n            this.powerButtonsContainer.setAttribute(\"dir\", direction);\n            // Hide/show buttons based on their availability.\n            for (const [{ isAvailable }, buttonElement] of this.descriptionToElementMap.entries()) {\n                const shouldHide = Boolean(isAvailable && !isAvailable(editableSelection));\n                buttonElement.classList.toggle(\"d-none\", shouldHide); // 2nd arg must be a boolean\n            }\n            this.setPowerButtonsPosition(block, direction);\n        }\n    }\n\n    /**\n     *\n     * @param {HTMLElement} block\n     * @param {string} direction\n     */\n    setPowerButtonsPosition(block, direction) {\n        const overlayStyles = this.powerButtonsOverlay.style;\n        // Resetting the position of the power buttons.\n        overlayStyles.top = \"0px\";\n        overlayStyles.left = \"0px\";\n        const blockRect = block.getBoundingClientRect();\n        const buttonsRect = this.powerButtonsContainer.getBoundingClientRect();\n        if (direction === \"rtl\") {\n            overlayStyles.left =\n                blockRect.right -\n                buttonsRect.width -\n                buttonsRect.x -\n                buttonsRect.width * 0.85 +\n                \"px\";\n        } else {\n            overlayStyles.left = blockRect.left - buttonsRect.x + buttonsRect.width * 0.85 + \"px\";\n        }\n        overlayStyles.top = blockRect.top - buttonsRect.top + \"px\";\n        overlayStyles.height = blockRect.height + \"px\";\n    }\n\n    /**\n     * @param {Function} commandFn\n     */\n    async applyCommand(commandFn) {\n        const btns = [...this.powerButtonsContainer.querySelectorAll(\".btn\")];\n        btns.forEach((btn) => btn.classList.add(\"disabled\"));\n        await commandFn();\n        btns.forEach((btn) => btn.classList.remove(\"disabled\"));\n    }\n}\n", "import { Component, onPatched, useEffect, useExternalListener, useRef } from \"@odoo/owl\";\n\n/**\n * @todo @phoenix i think that most of the \"control\" code in this component\n * should move to the powerbox plugin instead. This would probably be more robust\n */\nexport class Powerbox extends Component {\n    static template = \"html_editor.Powerbox\";\n    static props = {\n        document: { validate: (doc) => doc.constructor.name === \"HTMLDocument\" },\n        close: Function,\n        state: Object,\n        activateCommand: Function,\n        applyCommand: Function,\n    };\n\n    setup() {\n        const ref = useRef(\"root\");\n\n        onPatched(() => {\n            const activeCommand = ref.el.querySelector(\".o-we-command.active\");\n            if (activeCommand) {\n                activeCommand.scrollIntoView({ block: \"nearest\", inline: \"nearest\" });\n            }\n        });\n\n        this.mouseSelectionActive = false;\n        const onMouseMove = () => (this.mouseSelectionActive = true);\n        useExternalListener(this.props.document, \"mousemove\", onMouseMove);\n\n        // If necessary attach the same listener on the document on which\n        // the powerbox is mounted, serving the same purpose:\n        // do not trigger re-renderings when we are scrolling the powerbox\n        useEffect(\n            (ownDoc, propsDoc) => {\n                if (ownDoc && propsDoc && ownDoc !== propsDoc) {\n                    ownDoc.addEventListener(\"mousemove\", onMouseMove);\n                    return () => ownDoc.removeEventListener(\"mousemove\", onMouseMove);\n                }\n            },\n            () => [ref.el?.ownerDocument, this.props.document]\n        );\n    }\n\n    get commands() {\n        return this.props.state.commands;\n    }\n\n    get currentIndex() {\n        return this.props.state.currentIndex;\n    }\n\n    get showCategories() {\n        return this.props.state.showCategories;\n    }\n\n    onScroll() {\n        this.mouseSelectionActive = false;\n    }\n\n    onMouseEnter(index) {\n        if (this.mouseSelectionActive) {\n            this.props.activateCommand(index);\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { isEmptyBlock } from \"@html_editor/utils/dom_info\";\nimport { reactive } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rotate } from \"@web/core/utils/arrays\";\nimport { Powerbox } from \"./powerbox\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { omit, pick } from \"@web/core/utils/objects\";\nimport { baseContainerGlobalSelector } from \"@html_editor/utils/base_container\";\n\n/** @typedef { import(\"@html_editor/core/selection_plugin\").EditorSelection } EditorSelection */\n/** @typedef { import(\"@html_editor/core/user_command_plugin\").UserCommand } UserCommand */\n/** @typedef { ReturnType<_t> } TranslatedString */\n\n/**\n * @typedef {Object} PowerboxCategory\n * @property {string} id\n * @property {String} name\n *\n *\n * @typedef {Object} PowerboxItem\n * @property {string} categoryId Id of a powerbox category\n * @property {string} commandId Id of a user command to extend\n * @property {Object} [commandParams] Passed to the command's `run` function - optional\n * @property {TranslatedString} [title] Inheritable\n * @property {TranslatedString} [description] Inheritable\n * @property {string} [icon] fa-class - Inheritable\n * @property {TranslatedString[]} [keywords]\n * @property {(selection: EditorSelection) => boolean} [isAvailable] Optional and inheritable\n */\n\n/**\n * A powerbox item must derive from a user command ( @see UserCommand )\n * specified by commandId. Properties defined in a powerbox item override those\n * from a user command.\n *\n * Example:\n *\n * resources = {\n *      user_commands: [\n *          @type {UserCommand}\n *          {\n *              id: myCommand,\n *              run: myCommandFunction,\n *              title: _t(\"My Command\"),\n *              description: _t(\"My command's description\"),\n *              icon: \"fa-bug\",\n *          },\n *      ],\n *      powerbox_categories: [\n *          @type {PowerboxCategory}\n *          { id: \"myCategory\", name: _t(\"My Category\") }\n *      ],\n *      powerbox_items: [\n *          @type {PowerboxItem}\n *          {\n *              categoryId: \"myCategory\",\n *              commandId: \"myCommand\",\n *              title: _t(\"My Powerbox Command\"), // overrides the user command's `title`\n *              // `description` and `icon` are inferred from the user command\n *          }\n *      ],\n * };\n */\n\n/**\n * The resulting powerbox command after deriving properties from a user command\n * (type for internal use).\n * @typedef {Object} PowerboxCommand\n * @property {string} categoryId\n * @property {string} categoryName\n * @property {string} title\n * @property {string} description\n * @property {string} icon\n * @property {Function} run\n * @property {TranslatedString[]} [keywords]\n * @property { (selection: EditorSelection) => boolean  } [isAvailable]\n */\n\n/**\n * @param {SelectionData} selectionData\n */\nfunction target(selectionData) {\n    const node = selectionData.editableSelection.anchorNode;\n    const el = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;\n    if (\n        selectionData.documentSelectionIsInEditable &&\n        el.matches(baseContainerGlobalSelector) &&\n        isEmptyBlock(el)\n    ) {\n        return el;\n    }\n}\n\n/**\n * @typedef { Object } PowerboxShared\n * @property { PowerboxPlugin['closePowerbox'] } closePowerbox\n * @property { PowerboxPlugin['getAvailablePowerboxCommands'] } getAvailablePowerboxCommands\n * @property { PowerboxPlugin['openPowerbox'] } openPowerbox\n * @property { PowerboxPlugin['updatePowerbox'] } updatePowerbox\n */\n\nexport class PowerboxPlugin extends Plugin {\n    static id = \"powerbox\";\n    static dependencies = [\"overlay\", \"selection\", \"history\", \"userCommand\"];\n    static shared = [\n        \"closePowerbox\",\n        \"getAvailablePowerboxCommands\",\n        \"openPowerbox\",\n        \"updatePowerbox\",\n    ];\n    resources = {\n        user_commands: {\n            id: \"openPowerbox\",\n            run: () =>\n                this.openPowerbox({\n                    commands: this.getAvailablePowerboxCommands(),\n                    categories: this.getResource(\"powerbox_categories\"),\n                }),\n        },\n        powerbox_categories: [\n            withSequence(10, { id: \"structure\", name: _t(\"Structure\") }),\n            withSequence(60, { id: \"widget\", name: _t(\"Widget\") }),\n        ],\n        power_buttons: withSequence(100, {\n            commandId: \"openPowerbox\",\n            title: _t(\"More options\"),\n            icon: \"fa-ellipsis-v\",\n        }),\n        hints: {\n            text: _t('Type \"/\" for commands'),\n            target,\n        },\n    };\n\n    setup() {\n        /** @type {import(\"@html_editor/core/overlay_plugin\").Overlay} */\n        this.overlay = this.dependencies.overlay.createOverlay(Powerbox);\n\n        this.state = reactive({});\n        this.overlayProps = {\n            document: this.document,\n            close: () => this.overlay.close(),\n            state: this.state,\n            activateCommand: (currentIndex) => {\n                this.state.currentIndex = currentIndex;\n            },\n            applyCommand: this.applyCommand.bind(this),\n        };\n        this.powerboxCommands = this.makePowerboxCommands();\n        this.addDomListener(this.editable.ownerDocument, \"keydown\", this.onKeyDown);\n    }\n\n    /**\n     * @returns {PowerboxCommand[]}\n     */\n    getAvailablePowerboxCommands() {\n        const selection = this.dependencies.selection.getEditableSelection();\n        return this.powerboxCommands.filter(\n            (cmd) => cmd.isAvailable === undefined || cmd.isAvailable(selection)\n        );\n    }\n\n    /**\n     * @returns {PowerboxCommand[]}\n     */\n    makePowerboxCommands() {\n        /** @type {PowerboxItem[]} */\n        const powerboxItems = this.getResource(\"powerbox_items\");\n        /** @type {PowerboxCategory[]} */\n        const categories = this.getResource(\"powerbox_categories\");\n        const categoryDict = Object.fromEntries(\n            categories.map((category) => [category.id, category])\n        );\n        return powerboxItems.map((/** @type {PowerboxItem} */ item) => {\n            const command = this.dependencies.userCommand.getCommand(item.commandId);\n            return {\n                ...pick(command, \"title\", \"description\", \"icon\", \"isAvailable\"),\n                ...omit(item, \"commandId\", \"commandParams\"),\n                categoryName: categoryDict[item.categoryId].name,\n                run: () => command.run(item.commandParams),\n            };\n        });\n    }\n\n    /**\n     * @param {Object} params\n     * @param {PowerboxCommand[]} params.commands\n     * @param {PowerboxCategory[]} [params.categories]\n     * @param {Function} [params.onApplyCommand=() => {}]\n     * @param {Function} [params.onClose=() => {}]\n     */\n    openPowerbox({ commands, categories, onApplyCommand = () => {}, onClose = () => {} } = {}) {\n        this.closePowerbox();\n        this.onApplyCommand = onApplyCommand;\n        this.onClose = onClose;\n        this.updatePowerbox(commands, categories);\n    }\n\n    /**\n     * @param {PowerboxCommand[]} commands\n     * @param {PowerboxCategory[]} [categories]\n     */\n    updatePowerbox(commands, categories) {\n        if (categories) {\n            const orderCommands = [];\n            for (const category of categories) {\n                orderCommands.push(\n                    ...commands.filter((command) => command.categoryId === category.id)\n                );\n            }\n            commands = orderCommands;\n        }\n        Object.assign(this.state, {\n            showCategories: !!categories,\n            commands,\n            currentIndex: 0,\n        });\n        this.overlay.open({ props: this.overlayProps });\n    }\n\n    closePowerbox() {\n        if (!this.overlay.isOpen) {\n            return;\n        }\n        this.onClose();\n        this.overlay.close();\n    }\n\n    onKeyDown(ev) {\n        if (!this.overlay.isOpen) {\n            return;\n        }\n        const key = ev.key;\n        switch (key) {\n            case \"Escape\":\n                this.closePowerbox();\n                break;\n            case \"Enter\":\n            case \"Tab\":\n                ev.preventDefault();\n                ev.stopImmediatePropagation();\n                this.applyCommand(this.state.commands[this.state.currentIndex]);\n                break;\n            case \"ArrowUp\": {\n                ev.preventDefault();\n                this.state.currentIndex = rotate(this.state.currentIndex, this.state.commands, -1);\n                break;\n            }\n            case \"ArrowDown\": {\n                ev.preventDefault();\n                this.state.currentIndex = rotate(this.state.currentIndex, this.state.commands, 1);\n                break;\n            }\n            case \"ArrowLeft\":\n            case \"ArrowRight\": {\n                this.closePowerbox();\n                break;\n            }\n        }\n    }\n\n    applyCommand(command) {\n        this.onApplyCommand(command);\n        command.run();\n        this.closePowerbox();\n    }\n}\n", "import { fuzzyLookup } from \"@web/core/utils/search\";\nimport { Plugin } from \"../../plugin\";\n\n/**\n * @typedef {import(\"./powerbox_plugin\").PowerboxCategory} CommandGroup\n * @typedef {import(\"../core/selection_plugin\").EditorSelection} EditorSelection\n */\n\nexport class SearchPowerboxPlugin extends Plugin {\n    static id = \"searchPowerbox\";\n    static dependencies = [\"powerbox\", \"selection\", \"history\", \"input\"];\n    resources = {\n        beforeinput_handlers: this.onBeforeInput.bind(this),\n        input_handlers: this.onInput.bind(this),\n        delete_handlers: this.update.bind(this),\n        post_undo_handlers: this.update.bind(this),\n        post_redo_handlers: this.update.bind(this),\n    };\n    setup() {\n        const categoryIds = new Set();\n        for (const category of this.getResource(\"powerbox_categories\")) {\n            if (categoryIds.has(category.id)) {\n                throw new Error(`Duplicate category id: ${category.id}`);\n            }\n            categoryIds.add(category.id);\n        }\n        this.categories = this.getResource(\"powerbox_categories\");\n        this.shouldUpdate = false;\n    }\n    onBeforeInput(ev) {\n        if (ev.data === \"/\") {\n            this.historySavePointRestore = this.dependencies.history.makeSavePoint();\n        }\n    }\n    onInput(ev) {\n        if (ev.data === \"/\") {\n            this.openPowerbox();\n        } else {\n            this.update();\n        }\n    }\n    update() {\n        if (!this.shouldUpdate) {\n            return;\n        }\n        const selection = this.dependencies.selection.getEditableSelection();\n        this.searchNode = selection.startContainer;\n        if (!this.isSearching(selection)) {\n            this.dependencies.powerbox.closePowerbox();\n            return;\n        }\n        const searchTerm = this.searchNode.nodeValue.slice(this.offset + 1, selection.endOffset);\n        if (!searchTerm) {\n            this.dependencies.powerbox.updatePowerbox(this.enabledCommands, this.categories);\n            return;\n        }\n        if (searchTerm.includes(\" \")) {\n            this.dependencies.powerbox.closePowerbox();\n            return;\n        }\n        const commands = this.filterCommands(searchTerm);\n        if (!commands.length) {\n            this.dependencies.powerbox.closePowerbox();\n            this.shouldUpdate = true;\n            return;\n        }\n        this.dependencies.powerbox.updatePowerbox(commands);\n    }\n    /**\n     * @param {string} searchTerm\n     */\n    filterCommands(searchTerm) {\n        return fuzzyLookup(searchTerm, this.enabledCommands, (cmd) => [\n            cmd.title,\n            cmd.categoryName,\n            cmd.description,\n            ...(cmd.keywords || []),\n        ]);\n    }\n    /**\n     * @param {EditorSelection} selection\n     */\n    isSearching(selection) {\n        return (\n            selection.endContainer === this.searchNode &&\n            this.searchNode.nodeValue &&\n            this.searchNode.nodeValue[this.offset] === \"/\" &&\n            selection.endOffset >= this.offset\n        );\n    }\n    openPowerbox() {\n        const selection = this.dependencies.selection.getEditableSelection();\n        this.offset = selection.startOffset - 1;\n        this.enabledCommands = this.dependencies.powerbox.getAvailablePowerboxCommands();\n        this.dependencies.powerbox.openPowerbox({\n            commands: this.enabledCommands,\n            categories: this.categories,\n            onApplyCommand: this.historySavePointRestore,\n            onClose: () => {\n                this.shouldUpdate = false;\n            },\n        });\n        this.shouldUpdate = true;\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { parseHTML } from \"@html_editor/utils/html\";\nimport { user } from \"@web/core/user\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { renderToString } from \"@web/core/utils/render\";\nimport { markup } from \"@odoo/owl\";\nimport { isEmptyBlock, paragraphRelatedElementsSelector } from \"@html_editor/utils/dom_info\";\n\nexport const SIGNATURE_CLASS = \"o-signature-container\";\n\nexport class SignaturePlugin extends Plugin {\n    static id = \"signature\";\n    static dependencies = [\"dom\", \"history\", \"selection\"];\n    static shared = [\"cleanSignatures\"];\n    resources = {\n        user_commands: [\n            {\n                id: \"insertSignature\",\n                title: _t(\"Signature\"),\n                description: _t(\"Insert your signature\"),\n                icon: \"fa-pencil-square-o\",\n                run: this.insertSignature.bind(this),\n            },\n        ],\n        powerbox_categories: withSequence(100, { id: \"basic_block\", name: _t(\"Basic Bloc\") }),\n        powerbox_items: [\n            {\n                categoryId: \"basic_block\",\n                commandId: \"insertSignature\",\n            },\n        ],\n        is_empty_predicates: this.isEmpty.bind(this),\n        unsplittable_node_predicates: (host) =>\n            host.nodeType === Node.ELEMENT_NODE && host.matches(`.${SIGNATURE_CLASS}`),\n    };\n\n    cleanSignatures({ rootClone }) {\n        for (const el of rootClone.querySelectorAll(`.${SIGNATURE_CLASS}`)) {\n            el.remove();\n        }\n    }\n\n    async insertSignature() {\n        const [currentUser] = await this.services.orm.read(\n            \"res.users\",\n            [user.userId],\n            [\"signature\"]\n        );\n        if (currentUser && currentUser.signature) {\n            // User signature is sanitized in backend.\n            const signatureFragment = parseHTML(\n                this.document,\n                renderToString(\"html_editor.Signature\", {\n                    signature: markup(currentUser.signature),\n                    signatureClass: SIGNATURE_CLASS,\n                })\n            );\n            const signatureBlock = signatureFragment.firstElementChild;\n            this.dependencies.dom.insert(signatureFragment);\n            if (signatureBlock) {\n                const lastPhrasingElement = [\n                    ...signatureBlock.querySelectorAll(paragraphRelatedElementsSelector),\n                ].at(-1);\n                if (lastPhrasingElement) {\n                    this.dependencies.selection.setCursorEnd(lastPhrasingElement);\n                } else {\n                    this.dependencies.selection.setCursorEnd(signatureBlock);\n                }\n            }\n            this.dependencies.history.addStep();\n        }\n    }\n\n    isEmpty(element) {\n        if (\n            element.nodeType === Node.ELEMENT_NODE &&\n            element.matches(`.${SIGNATURE_CLASS}`) &&\n            isEmptyBlock(element)\n        ) {\n            return true;\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport {\n    getAdjacentNextSiblings,\n    getAdjacentPreviousSiblings,\n} from \"@html_editor/utils/dom_traversal\";\nimport { parseHTML } from \"@html_editor/utils/html\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class StarPlugin extends Plugin {\n    static id = \"star\";\n    static dependencies = [\"dom\", \"history\"];\n    resources = {\n        user_commands: [\n            {\n                id: \"addStars\",\n                title: _t(\"Stars\"),\n                description: _t(\"Insert a rating\"),\n                icon: \"fa-star\",\n                run: this.addStars.bind(this),\n            },\n        ],\n        powerbox_items: [\n            {\n                title: _t(\"3 Stars\"),\n                description: _t(\"Insert a rating over 3 stars\"),\n                categoryId: \"widget\",\n                icon: \"fa-star-o\",\n                commandId: \"addStars\",\n                commandParams: { length: 3 },\n            },\n            {\n                title: _t(\"5 Stars\"),\n                description: _t(\"Insert a rating over 5 stars\"),\n                categoryId: \"widget\",\n                commandId: \"addStars\",\n                commandParams: { length: 5 },\n            },\n        ],\n    };\n\n    setup() {\n        this.addDomListener(this.editable, \"pointerdown\", this.onMouseDown);\n    }\n\n    onMouseDown(ev) {\n        const node = ev.target;\n        const isStar = (node) =>\n            node.nodeType === Node.ELEMENT_NODE &&\n            (node.classList.contains(\"fa-star\") || node.classList.contains(\"fa-star-o\"));\n        if (\n            isStar(node) &&\n            node.parentElement &&\n            node.parentElement.className.includes(\"o_stars\")\n        ) {\n            const previousStars = getAdjacentPreviousSiblings(node, isStar);\n            const nextStars = getAdjacentNextSiblings(node, isStar);\n            if (nextStars.length || previousStars.length) {\n                const shouldToggleOff =\n                    node.classList.contains(\"fa-star\") &&\n                    (!nextStars[0] || !nextStars[0].classList.contains(\"fa-star\"));\n                for (const star of [...previousStars, node]) {\n                    star.classList.toggle(\"fa-star-o\", shouldToggleOff);\n                    star.classList.toggle(\"fa-star\", !shouldToggleOff);\n                }\n                for (const star of nextStars) {\n                    star.classList.toggle(\"fa-star-o\", true);\n                    star.classList.toggle(\"fa-star\", false);\n                }\n                this.dependencies.history.addStep();\n            }\n            ev.stopPropagation();\n            ev.preventDefault();\n        }\n    }\n\n    addStars({ length }) {\n        const stars = Array.from({ length }, () => '<i class=\"fa fa-star-o\"></i>').join(\"\");\n        const html = `\\u200B<span contenteditable=\"false\" class=\"o_stars\">${stars}</span>\\u200B`;\n        this.dependencies.dom.insert(parseHTML(this.document, html));\n        this.dependencies.history.addStep();\n    }\n}\n", "import { Component } 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\";\n\nexport class TableMenu extends Component {\n    static template = \"html_editor.TableMenu\";\n    static props = {\n        type: String, // column or row\n        moveColumn: Function,\n        addColumn: Function,\n        removeColumn: Function,\n        moveRow: Function,\n        addRow: Function,\n        removeRow: Function,\n        resetTableSize: Function,\n        overlay: Object,\n        dropdownState: Object,\n        target: { validate: (el) => el.nodeType === Node.ELEMENT_NODE },\n        direction: { type: String, optional: true },\n    };\n    static defaultProps = { direction: \"ltr\" };\n    static components = { Dropdown, DropdownItem };\n\n    setup() {\n        if (this.props.type === \"column\") {\n            this.isFirst = this.props.target.cellIndex === 0;\n            this.isLast = !this.props.target.nextElementSibling;\n        } else {\n            const tr = this.props.target.parentElement;\n            this.isFirst = !tr.previousElementSibling;\n            this.isLast = !tr.nextElementSibling;\n        }\n        this.items = this.props.type === \"column\" ? this.colItems() : this.rowItems();\n    }\n\n    get hasCustomSize() {\n        return (\n            !!this.props.target.closest(\"tr\").style.height ||\n            !!this.props.target.closest(\"td\")?.style?.width ||\n            !!this.props.target.closest(\"th\")?.style?.width\n        );\n    }\n\n    onSelected(item) {\n        item.action(this.props.target);\n        this.props.overlay.close();\n    }\n\n    colItems() {\n        const ltr = this.props.direction === \"ltr\";\n        return [\n            !this.isFirst && {\n                name: \"move_left\",\n                icon: \"fa-chevron-left disabled\",\n                text: ltr ? _t(\"Move left\") : _t(\"Move right\"),\n                action: this.props.moveColumn.bind(this, \"left\"),\n            },\n            !this.isLast && {\n                name: \"move_right\",\n                icon: \"fa-chevron-right\",\n                text: ltr ? _t(\"Move right\") : _t(\"Move left\"),\n                action: this.props.moveColumn.bind(this, \"right\"),\n            },\n            {\n                name: \"insert_left\",\n                icon: \"fa-plus\",\n                text: ltr ? _t(\"Insert left\") : _t(\"Insert right\"),\n                action: this.props.addColumn.bind(this, \"before\"),\n            },\n            {\n                name: \"insert_right\",\n                icon: \"fa-plus\",\n                text: ltr ? _t(\"Insert right\") : _t(\"Insert left\"),\n                action: this.props.addColumn.bind(this, \"after\"),\n            },\n            {\n                name: \"delete\",\n                icon: \"fa-trash\",\n                text: _t(\"Delete\"),\n                action: this.props.removeColumn.bind(this),\n            },\n            this.hasCustomSize && {\n                name: \"reset_size\",\n                icon: \"fa-table\",\n                text: _t(\"Reset Size\"),\n                action: (target) => this.props.resetTableSize(target.closest(\"table\")),\n            },\n        ].filter(Boolean);\n    }\n\n    rowItems() {\n        return [\n            !this.isFirst && {\n                name: \"move_up\",\n                icon: \"fa-chevron-up\",\n                text: _t(\"Move up\"),\n                action: (target) => this.props.moveRow(\"up\", target.parentElement),\n            },\n            !this.isLast && {\n                name: \"move_down\",\n                icon: \"fa-chevron-down\",\n                text: _t(\"Move down\"),\n                action: (target) => this.props.moveRow(\"down\", target.parentElement),\n            },\n            {\n                name: \"insert_above\",\n                icon: \"fa-plus\",\n                text: _t(\"Insert above\"),\n                action: (target) => this.props.addRow(\"before\", target.parentElement),\n            },\n            {\n                name: \"insert_below\",\n                icon: \"fa-plus\",\n                text: _t(\"Insert below\"),\n                action: (target) => this.props.addRow(\"after\", target.parentElement),\n            },\n            {\n                name: \"delete\",\n                icon: \"fa-trash\",\n                text: _t(\"Delete\"),\n                action: (target) => this.props.removeRow(target.parentElement),\n            },\n            this.hasCustomSize && {\n                name: \"reset_size\",\n                icon: \"fa-table\",\n                text: _t(\"Reset Size\"),\n                action: (target) => this.props.resetTableSize(target.closest(\"table\")),\n            },\n        ].filter(Boolean);\n    }\n}\n", "import { Component, useExternalListener, useState } from \"@odoo/owl\";\n\nexport class TablePicker extends Component {\n    static template = \"html_editor.TablePicker\";\n    static props = {\n        insertTable: Function,\n        editable: {\n            validate: (el) => el.nodeType === Node.ELEMENT_NODE,\n        },\n        overlay: Object,\n        direction: String,\n    };\n\n    setup() {\n        this.state = useState({\n            cols: 3,\n            rows: 3,\n        });\n        useExternalListener(this.props.editable.ownerDocument, \"keydown\", (ev) => {\n            const key = ev.key;\n            const isRTL = this.props.direction === \"rtl\";\n            switch (key) {\n                case \"Enter\":\n                    ev.preventDefault();\n                    this.insertTable();\n                    break;\n                case \"ArrowUp\":\n                    ev.preventDefault();\n                    if (this.state.rows > 1) {\n                        this.state.rows -= 1;\n                    }\n                    break;\n                case \"ArrowDown\":\n                    this.state.rows += 1;\n                    ev.preventDefault();\n                    break;\n                case \"ArrowLeft\":\n                    ev.preventDefault();\n                    if (isRTL) {\n                        this.state.cols += 1;\n                    } else {\n                        if (this.state.cols > 1) {\n                            this.state.cols -= 1;\n                        }\n                    }\n                    break;\n                case \"ArrowRight\":\n                    ev.preventDefault();\n                    if (isRTL) {\n                        if (this.state.cols > 1) {\n                            this.state.cols -= 1;\n                        }\n                    } else {\n                        this.state.cols += 1;\n                    }\n                    break;\n                default:\n                    this.props.overlay.close();\n                    break;\n            }\n        });\n    }\n\n    updateSize(cols, rows) {\n        this.state.cols = cols;\n        this.state.rows = rows;\n    }\n\n    insertTable() {\n        this.props.insertTable({ cols: this.state.cols, rows: this.state.rows });\n        this.props.overlay.close();\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { baseContainerGlobalSelector } from \"@html_editor/utils/base_container\";\nimport { isBlock } from \"@html_editor/utils/blocks\";\nimport { fillShrunkPhrasingParent, removeClass, splitTextNode } from \"@html_editor/utils/dom\";\nimport {\n    getDeepestPosition,\n    isProtected,\n    isProtecting,\n    isEmptyBlock,\n} from \"@html_editor/utils/dom_info\";\nimport { ancestors, closestElement, descendants, lastLeaf } from \"@html_editor/utils/dom_traversal\";\nimport { parseHTML } from \"@html_editor/utils/html\";\nimport { DIRECTIONS, leftPos, rightPos, nodeSize } from \"@html_editor/utils/position\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { findInSelection } from \"@html_editor/utils/selection\";\nimport { getColumnIndex, getRowIndex } from \"@html_editor/utils/table\";\nimport { isBrowserFirefox } from \"@web/core/browser/feature_detection\";\n\nexport const BORDER_SENSITIVITY = 5;\n\nconst tableInnerComponents = new Set([\"THEAD\", \"TBODY\", \"TFOOT\", \"TR\", \"TH\", \"TD\"]);\nfunction isUnremovableTableComponent(node, root) {\n    if (!tableInnerComponents.has(node.nodeName)) {\n        return false;\n    }\n    if (!root) {\n        return true;\n    }\n    const closestTable = closestElement(node, \"table\");\n    return !root.contains(closestTable);\n}\n\n/**\n * @typedef { Object } TableShared\n * @property { TablePlugin['addColumn'] } addColumn\n * @property { TablePlugin['addRow'] } addRow\n * @property { TablePlugin['moveColumn'] } moveColumn\n * @property { TablePlugin['moveRow'] } moveRow\n * @property { TablePlugin['removeColumn'] } removeColumn\n * @property { TablePlugin['removeRow'] } removeRow\n * @property { TablePlugin['resetTableSize'] } resetTableSize\n */\n\n/**\n * This plugin only contains the table manipulation and selection features. All UI overlay\n * code is located in the table_ui plugin\n */\nexport class TablePlugin extends Plugin {\n    static id = \"table\";\n    static dependencies = [\n        \"baseContainer\",\n        \"dom\",\n        \"history\",\n        \"selection\",\n        \"delete\",\n        \"split\",\n        \"color\",\n    ];\n    static shared = [\n        \"insertTable\",\n        \"addColumn\",\n        \"addRow\",\n        \"removeColumn\",\n        \"removeRow\",\n        \"moveColumn\",\n        \"moveRow\",\n        \"resetTableSize\",\n    ];\n    resources = {\n        user_commands: [\n            {\n                id: \"insertTable\",\n                run: (params) => {\n                    this.insertTable(params);\n                },\n            },\n        ],\n\n        /** Handlers */\n        selectionchange_handlers: this.updateSelectionTable.bind(this),\n        clean_handlers: this.deselectTable.bind(this),\n        clean_for_save_handlers: ({ root }) => this.deselectTable(root),\n        before_line_break_handlers: this.resetTableSelection.bind(this),\n        before_split_block_handlers: this.resetTableSelection.bind(this),\n\n        /** Overrides */\n        tab_overrides: withSequence(20, this.handleTab.bind(this)),\n        shift_tab_overrides: withSequence(20, this.handleShiftTab.bind(this)),\n        delete_range_overrides: this.handleDeleteRange.bind(this),\n        color_apply_overrides: this.applyTableColor.bind(this),\n\n        unremovable_node_predicates: isUnremovableTableComponent,\n        unsplittable_node_predicates: (node) =>\n            node.nodeName === \"TABLE\" || tableInnerComponents.has(node.nodeName),\n        fully_selected_node_predicates: (node) => !!closestElement(node, \".o_selected_td\"),\n        traversed_nodes_processors: this.adjustTraversedNodes.bind(this),\n    };\n\n    setup() {\n        this.addDomListener(this.editable, \"mousedown\", this.onMousedown);\n        this.addDomListener(this.editable, \"mouseup\", this.onMouseup);\n        this.addDomListener(this.editable, \"keydown\", (ev) => {\n            this._isKeyDown = true;\n        });\n        this.onMousemove = this.onMousemove.bind(this);\n    }\n\n    handleTab() {\n        const selection = this.dependencies.selection.getEditableSelection();\n        const inTable = closestElement(selection.anchorNode, \"table\");\n        if (inTable) {\n            // Move cursor to next cell.\n            const shouldAddNewRow = !this.shiftCursorToTableCell(1);\n            if (shouldAddNewRow) {\n                this.addRow(\"after\", findInSelection(selection, \"tr\"));\n                this.shiftCursorToTableCell(1);\n                this.dependencies.history.addStep();\n            }\n            return true;\n        }\n    }\n\n    handleShiftTab() {\n        const selection = this.dependencies.selection.getEditableSelection();\n        const inTable = closestElement(selection.anchorNode, \"table\");\n        if (inTable) {\n            // Move cursor to previous cell.\n            this.shiftCursorToTableCell(-1);\n            return true;\n        }\n    }\n\n    createTable({ rows = 2, cols = 2 } = {}) {\n        const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n        fillShrunkPhrasingParent(baseContainer);\n        const baseContainerHtml = baseContainer.outerHTML;\n        const tdsHtml = new Array(cols).fill(`<td>${baseContainerHtml}</td>`).join(\"\");\n        const trsHtml = new Array(rows).fill(`<tr>${tdsHtml}</tr>`).join(\"\");\n        const tableHtml = `<table class=\"table table-bordered o_table\"><tbody>${trsHtml}</tbody></table>`;\n        return parseHTML(this.document, tableHtml);\n    }\n\n    _insertTable({ rows = 2, cols = 2 } = {}) {\n        const newTable = this.createTable({ rows, cols });\n        let sel = this.dependencies.selection.getEditableSelection();\n        if (!sel.isCollapsed) {\n            this.dependencies.delete.deleteSelection();\n        }\n        while (!isBlock(sel.anchorNode)) {\n            const anchorNode = sel.anchorNode;\n            const isTextNode = anchorNode.nodeType === Node.TEXT_NODE;\n            const newAnchorNode = isTextNode\n                ? splitTextNode(anchorNode, sel.anchorOffset, DIRECTIONS.LEFT) + 1 && anchorNode\n                : this.dependencies.split.splitElement(anchorNode, sel.anchorOffset).shift();\n            const newPosition = rightPos(newAnchorNode);\n            sel = this.dependencies.selection.setSelection(\n                { anchorNode: newPosition[0], anchorOffset: newPosition[1] },\n                { normalize: false }\n            );\n        }\n        const [table] = this.dependencies.dom.insert(newTable);\n        return table;\n    }\n    insertTable({ rows = 2, cols = 2 } = {}) {\n        const table = this._insertTable({ rows, cols });\n        this.dependencies.selection.setCursorStart(\n            table.querySelector(baseContainerGlobalSelector)\n        );\n        this.dependencies.history.addStep();\n    }\n    /**\n     * @param {'before'|'after'} position\n     * @param {HTMLTableCellElement} reference\n     */\n    addColumn(position, reference) {\n        const columnIndex = getColumnIndex(reference);\n        const table = closestElement(reference, \"table\");\n        const tableWidth = table.style.width && parseFloat(table.style.width);\n        const referenceColumn = table.querySelectorAll(\n            `tr td:nth-of-type(${columnIndex + 1}), tr th:nth-of-type(${columnIndex + 1})`\n        );\n        const referenceCellWidth = reference.style.width\n            ? parseFloat(reference.style.width)\n            : reference.clientWidth;\n        // Temporarily set widths so proportions are respected.\n        const firstRow = table.querySelector(\"tr\");\n        const firstRowCells = [...firstRow.children].filter(\n            (child) => child.nodeName === \"TD\" || child.nodeName === \"TH\"\n        );\n        let totalWidth = 0;\n        if (tableWidth) {\n            for (const cell of firstRowCells) {\n                const width = parseFloat(cell.style.width);\n                cell.style.width = width + \"px\";\n                // Spread the widths to preserve proportions.\n                // -1 for the width of the border of the new column.\n                const newWidth = Math.max(\n                    Math.round((width * tableWidth) / (tableWidth + referenceCellWidth - 1)),\n                    13\n                );\n                cell.style.width = newWidth + \"px\";\n                totalWidth += newWidth;\n            }\n        }\n        referenceColumn.forEach((cell, rowIndex) => {\n            const newCell = this.document.createElement(cell.tagName);\n            const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n            baseContainer.append(this.document.createElement(\"br\"));\n            newCell.append(baseContainer);\n            cell[position](newCell);\n            if (rowIndex === 0 && tableWidth) {\n                newCell.style.width = cell.style.width;\n                totalWidth += parseFloat(cell.style.width);\n            }\n        });\n        if (tableWidth) {\n            if (totalWidth !== tableWidth - 1) {\n                // -1 for the width of the border of the new column.\n                firstRowCells[firstRowCells.length - 1].style.width =\n                    parseFloat(firstRowCells[firstRowCells.length - 1].style.width) +\n                    (tableWidth - totalWidth - 1) +\n                    \"px\";\n            }\n            // Fix the table and row's width so it doesn't change.\n            table.style.width = tableWidth + \"px\";\n        }\n    }\n    /**\n     * @param {'before'|'after'} position\n     * @param {HTMLTableRowElement} reference\n     */\n    addRow(position, reference) {\n        const referenceRowHeight = reference.style.height && parseFloat(reference.style.height);\n        const newRow = this.document.createElement(\"tr\");\n        if (referenceRowHeight) {\n            newRow.style.height = referenceRowHeight + \"px\";\n        }\n        const cells = reference.querySelectorAll(\"td, th\");\n        const referenceRowWidths = [...cells].map((cell) => cell.style.width);\n        newRow.append(\n            ...Array.from(cells).map((cell) => {\n                const td = this.document.createElement(cell.tagName);\n                const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n                baseContainer.append(this.document.createElement(\"br\"));\n                td.append(baseContainer);\n                return td;\n            })\n        );\n        reference[position](newRow);\n        if (referenceRowHeight) {\n            newRow.style.height = referenceRowHeight + \"px\";\n        }\n        // Preserve the width of the columns (applied only on the first row).\n        if (getRowIndex(newRow) === 0) {\n            let columnIndex = 0;\n            for (const column of newRow.children) {\n                column.style.width = referenceRowWidths[columnIndex];\n                cells[columnIndex].style.width = \"\";\n                columnIndex++;\n            }\n        }\n    }\n    /**\n     * @param {HTMLTableCellElement} cell\n     */\n    removeColumn(cell) {\n        const table = closestElement(cell, \"table\");\n        const cells = [...closestElement(cell, \"tr\").querySelectorAll(\"th, td\")];\n        const index = cells.findIndex((td) => td === cell);\n        const siblingCell = cells[index - 1] || cells[index + 1];\n        table.querySelectorAll(`tr td:nth-of-type(${index + 1})`).forEach((td) => td.remove());\n        // not sure we should move the cursor?\n        siblingCell\n            ? this.dependencies.selection.setCursorStart(siblingCell)\n            : this.deleteTable(table);\n    }\n    /**\n     * @param {HTMLTableRowElement} row\n     */\n    removeRow(row) {\n        const table = closestElement(row, \"table\");\n        const siblingRow = row.previousElementSibling || row.nextElementSibling;\n        row.remove();\n        // not sure we should move the cursor?\n        siblingRow\n            ? this.dependencies.selection.setCursorStart(siblingRow.querySelector(\"td\"))\n            : this.deleteTable(table);\n    }\n    /**\n     * @param {'left'|'right'} position\n     * @param {HTMLTableCellElement} cell\n     */\n    moveColumn(position, cell) {\n        const columnIndex = getColumnIndex(cell);\n        const nColumns = cell.parentElement.children.length;\n        if (\n            columnIndex < 0 ||\n            (position === \"left\" && columnIndex === 0) ||\n            (position !== \"left\" && columnIndex === nColumns - 1)\n        ) {\n            return;\n        }\n\n        const trs = cell.parentElement.parentElement.children;\n        const tdsToMove = [...trs].map((tr) => tr.children[columnIndex]);\n        const selectionToRestore = this.dependencies.selection.getEditableSelection();\n        if (position === \"left\") {\n            tdsToMove.forEach((td) => td.previousElementSibling.before(td));\n        } else {\n            tdsToMove.forEach((td) => td.nextElementSibling.after(td));\n        }\n        this.dependencies.selection.setSelection(selectionToRestore);\n    }\n    /**\n     * @param {'up'|'down'} position\n     * @param {HTMLTableRowElement} row\n     */\n    moveRow(position, row) {\n        const selectionToRestore = this.dependencies.selection.getEditableSelection();\n        let adjustedRow;\n        if (position === \"up\") {\n            row.previousElementSibling?.before(row);\n            adjustedRow = row;\n        } else {\n            row.nextElementSibling?.after(row);\n            adjustedRow = row.previousElementSibling;\n        }\n\n        // If the moved row becomes the first row, copy the widths of its td\n        // elements from the previous first row, as td widths are only applied\n        // to the first row.\n        if (!adjustedRow.previousElementSibling) {\n            adjustedRow.childNodes.forEach((cell, index) => {\n                cell.style.width = adjustedRow.nextElementSibling.childNodes[index].style.width;\n            });\n        }\n        this.dependencies.selection.setSelection(selectionToRestore);\n    }\n    /**\n     * @param {HTMLTableElement} table\n     */\n    resetTableSize(table) {\n        table.removeAttribute(\"style\");\n        const cells = [...table.querySelectorAll(\"tr, td\")];\n        cells.forEach((cell) => {\n            const cStyle = cell.style;\n            if (cell.tagName === \"TR\") {\n                cStyle.height = \"\";\n            } else {\n                cStyle.width = \"\";\n            }\n        });\n    }\n    deleteTable(table) {\n        table =\n            table || findInSelection(this.dependencies.selection.getEditableSelection(), \"table\");\n        if (!table) {\n            return;\n        }\n        const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n        baseContainer.appendChild(this.document.createElement(\"br\"));\n        table.before(baseContainer);\n        table.remove();\n        this.dependencies.selection.setCursorStart(baseContainer);\n    }\n\n    // @todo @phoenix: handle deleteBackward on table cells\n    // deleteBackwardBefore({ targetNode, targetOffset }) {\n    //     // If the cursor is at the beginning of a row, prevent deletion.\n    //     if (targetNode.nodeType === Node.ELEMENT_NODE && isRow(targetNode) && !targetOffset) {\n    //         return true;\n    //     }\n    // }\n\n    /**\n     * Removes fully selected rows or columns, clears the content of selected\n     * cells otherwise.\n     *\n     * @param {NodeListOf<HTMLTableCellElement>} selectedTds - Non-empty\n     * NodeList of selected table cells.\n     */\n    deleteTableCells(selectedTds) {\n        const rows = [...closestElement(selectedTds[0], \"tr\").parentElement.children].filter(\n            (child) => child.nodeName === \"TR\"\n        );\n        const firstRowCells = [...rows[0].children].filter(\n            (child) => child.nodeName === \"TD\" || child.nodeName === \"TH\"\n        );\n        const firstCellRowIndex = getRowIndex(selectedTds[0]);\n        const firstCellColumnIndex = getColumnIndex(selectedTds[0]);\n        const lastCellRowIndex = getRowIndex(selectedTds[selectedTds.length - 1]);\n        const lastCellColumnIndex = getColumnIndex(selectedTds[selectedTds.length - 1]);\n\n        const areFullColumnsSelected =\n            firstCellRowIndex === 0 && lastCellRowIndex === rows.length - 1;\n        const areFullRowsSelected =\n            firstCellColumnIndex === 0 && lastCellColumnIndex === firstRowCells.length - 1;\n\n        if (areFullColumnsSelected) {\n            for (let index = firstCellColumnIndex; index <= lastCellColumnIndex; index++) {\n                this.removeColumn(firstRowCells[index]);\n            }\n            return;\n        }\n\n        if (areFullRowsSelected) {\n            for (let index = firstCellRowIndex; index <= lastCellRowIndex; index++) {\n                this.removeRow(rows[index]);\n            }\n            return;\n        }\n\n        for (const td of selectedTds) {\n            // @todo @phoenix this replaces paragraphs by inline content. Is this intended?\n            td.replaceChildren(this.document.createElement(\"br\"));\n        }\n        this.dependencies.selection.setCursorStart(selectedTds[0]);\n    }\n\n    /**\n     * @param {Object} range - Range-like object.\n     * @param {Array} fullySelectedTables - Non-empty array of table elements.\n     */\n    deleteRangeWithFullySelectedTables(range, fullySelectedTables) {\n        let { startContainer, startOffset, endContainer, endOffset } = range;\n\n        // Expand range to fully include tables.\n        const firstTable = fullySelectedTables[0];\n        if (firstTable.contains(startContainer)) {\n            [startContainer, startOffset] = leftPos(firstTable);\n        }\n        const lastTable = fullySelectedTables.at(-1);\n        if (lastTable.contains(endContainer)) {\n            [endContainer, endOffset] = rightPos(lastTable);\n        }\n        range = { startContainer, startOffset, endContainer, endOffset };\n\n        range = this.dependencies.delete.deleteRange(range);\n\n        // Normalize deep.\n        // @todo @phoenix: Use something from the selection plugin (normalize deep?)\n        const [anchorNode, anchorOffset] = getDeepestPosition(\n            range.startContainer,\n            range.startOffset\n        );\n\n        this.dependencies.selection.setSelection({ anchorNode, anchorOffset });\n    }\n\n    handleDeleteRange(range) {\n        // @todo @phoenix: this does not depend on the range. This should be\n        // optimized by keeping in memory the state of selected cells/tables.\n        const fullySelectedTables = [...this.editable.querySelectorAll(\".o_selected_table\")].filter(\n            (table) =>\n                [...table.querySelectorAll(\"td\")].every(\n                    (td) =>\n                        closestElement(td, \"table\") !== table ||\n                        td.classList.contains(\"o_selected_td\")\n                )\n        );\n        if (fullySelectedTables.length) {\n            this.deleteRangeWithFullySelectedTables(range, fullySelectedTables);\n            return true;\n        }\n\n        const selectedTds = this.editable.querySelectorAll(\".o_selected_td\");\n        if (selectedTds.length) {\n            this.deleteTableCells(selectedTds);\n            // this._toggleTableUi();\n            return true;\n        }\n\n        return false;\n    }\n\n    /**\n     * Moves the cursor by shiftIndex table cells.\n     *\n     * @param {Number} shiftIndex - The index to shift the cursor by.\n     * @returns {boolean} - True if the cursor was successfully moved, false otherwise.\n     */\n    shiftCursorToTableCell(shiftIndex) {\n        const sel = this.dependencies.selection.getEditableSelection();\n        const currentTd = closestElement(sel.anchorNode, \"td\");\n        const closestTable = closestElement(currentTd, \"table\");\n        if (!currentTd || !closestTable) {\n            return false;\n        }\n        const tds = [...closestTable.querySelectorAll(\"td\")];\n        const cursorDestination = tds[tds.findIndex((td) => currentTd === td) + shiftIndex];\n        if (!cursorDestination) {\n            return false;\n        }\n        this.dependencies.selection.setCursorEnd(lastLeaf(cursorDestination));\n        return true;\n    }\n\n    hanldeFirefoxSelection(ev = null) {\n        const selection = this.document.getSelection();\n        if (isBrowserFirefox()) {\n            if (!this.dependencies.selection.isSelectionInEditable(selection)) {\n                return false;\n            }\n            if (selection.rangeCount > 1) {\n                // In Firefox, selecting multiple cells within a table using the mouse can create multiple ranges.\n                // This behavior can cause the original selection (where the selection started) to be lost.\n                // To solve the issue we merge the ranges of the selection together the first time we find\n                // selection.rangeCount > 1.\n                const [anchorNode, anchorOffset] = getDeepestPosition(\n                    selection.getRangeAt(0).startContainer,\n                    selection.getRangeAt(0).startOffset\n                );\n                const [focusNode, focusOffset] = getDeepestPosition(\n                    selection.getRangeAt(selection.rangeCount - 1).startContainer,\n                    selection.getRangeAt(selection.rangeCount - 1).startOffset\n                );\n                this.dependencies.selection.setSelection({\n                    anchorNode,\n                    anchorOffset,\n                    focusNode,\n                    focusOffset,\n                });\n                return true;\n            } else if (\n                ev &&\n                closestElement(ev.target, \"table\") ===\n                    closestElement(selection.anchorNode, \"table\") &&\n                closestElement(ev.target, \"td\") !== closestElement(selection.focusNode, \"td\")\n            ) {\n                // After the manual update firefox will not be able the table selection automatically\n                // so we need to update the selection manually too.\n                // When we hover on a new table cell we mark it as the new focusNode.\n                this.dependencies.selection.setSelection({\n                    anchorNode: selection.anchorNode,\n                    anchorOffset: selection.anchorOffset,\n                    focusNode: ev.target,\n                    focusOffset: 0,\n                });\n                return true;\n            }\n        }\n        return false;\n    }\n\n    updateSelectionTable(selectionData) {\n        if (this.hanldeFirefoxSelection()) {\n            // It will be retriggered with selectionchange\n            return;\n        }\n        const selection = selectionData.editableSelection;\n        const startTd = closestElement(selection.startContainer, \"td\");\n        const endTd = closestElement(selection.endContainer, \"td\");\n        if (!(startTd && startTd === endTd) || this._isKeyDown) {\n            delete this._isKeyDown;\n            // Prevent deselecting single cell unless selection changes\n            // through keyboard.\n            this.deselectTable();\n        }\n        const startTable = ancestors(selection.startContainer, this.editable)\n            .filter((node) => node.nodeName === \"TABLE\")\n            .pop();\n        const endTable = ancestors(selection.endContainer, this.editable)\n            .filter((node) => node.nodeName === \"TABLE\")\n            .pop();\n\n        const traversedNodes = this.dependencies.selection.getTraversedNodes({ deep: true });\n        if (startTd !== endTd && startTable === endTable) {\n            if (!isProtected(startTable) && !isProtecting(startTable)) {\n                // The selection goes through at least two different cells ->\n                // select cells.\n                this.selectTableCells(selection);\n            }\n        } else if (!traversedNodes.every((node) => closestElement(node.parentElement, \"table\"))) {\n            const traversedTables = new Set(\n                traversedNodes\n                    .map((node) => closestElement(node, \"table\"))\n                    .filter((node) => node && !isProtected(node) && !isProtecting(node))\n            );\n            for (const table of traversedTables) {\n                // Don't apply several nested levels of selection.\n                if (!ancestors(table, this.editable).some((node) => traversedTables.has(node))) {\n                    table.classList.toggle(\"o_selected_table\", true);\n                    for (const td of [...table.querySelectorAll(\"td\")].filter(\n                        (td) => closestElement(td, \"table\") === table\n                    )) {\n                        td.classList.toggle(\"o_selected_td\", true);\n                    }\n                }\n            }\n        }\n    }\n\n    onMousedown(ev) {\n        this._currentMouseState = ev.type;\n        this._lastMousedownPosition = [ev.x, ev.y];\n        this.deselectTable();\n        if (this.isPointerInsideCell(ev)) {\n            this.editable.addEventListener(\"mousemove\", this.onMousemove);\n            const currentSelection = this.dependencies.selection.getEditableSelection();\n            // disable dragging on table\n            this.dependencies.selection.setCursorStart(currentSelection.anchorNode);\n        }\n    }\n\n    onMouseup(ev) {\n        this._currentMouseState = ev.type;\n        this.editable.removeEventListener(\"mousemove\", this.onMousemove);\n    }\n\n    /**\n     * Checks if mouse is effectively inside the cell and not overlapping\n     * the cell borders to prevent cell selection while resizing table.\n     *\n     * @param {MouseEvent} ev\n     * @returns {Boolean}\n     */\n    isPointerInsideCell(ev) {\n        const td = closestElement(ev.target, \"td\");\n        if (td) {\n            const targetRect = td.getBoundingClientRect();\n            if (\n                ev.clientX > targetRect.x + BORDER_SENSITIVITY &&\n                ev.clientX < targetRect.x + td.clientWidth - BORDER_SENSITIVITY &&\n                ev.clientY > targetRect.y + BORDER_SENSITIVITY &&\n                ev.clientY < targetRect.y + td.clientHeight - BORDER_SENSITIVITY\n            ) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    onMousemove(ev) {\n        if (this._currentMouseState !== \"mousedown\") {\n            return;\n        }\n        if (this.hanldeFirefoxSelection(ev)) {\n            return;\n        }\n        const selection = this.dependencies.selection.getEditableSelection();\n        const docSelection = this.document.getSelection();\n        const range = docSelection.rangeCount && docSelection.getRangeAt(0);\n        const startTd = closestElement(selection.startContainer, \"td\");\n        const endTd = closestElement(selection.endContainer, \"td\");\n        if (startTd && startTd === endTd && !isProtected(startTd) && !isProtecting(startTd)) {\n            const selectedNodes = this.dependencies.selection.getSelectedNodes();\n            const cellContents = descendants(startTd);\n            const areCellContentsFullySelected = cellContents\n                .filter((d) => !isBlock(d))\n                .every((child) => selectedNodes.includes(child));\n            if (areCellContentsFullySelected) {\n                const SENSITIVITY = 5;\n                const rangeRect = range.getBoundingClientRect();\n                const isMovingAwayFromSelection =\n                    ev.clientX > rangeRect.x + rangeRect.width + SENSITIVITY || // moving right\n                    ev.clientX < rangeRect.x - SENSITIVITY; // moving left\n                if (isMovingAwayFromSelection) {\n                    // A cell is fully selected and the mouse is moving away\n                    // from the selection, within said cell -> select the cell.\n                    this.selectTableCells(selection);\n                }\n            } else if (\n                cellContents.filter(isBlock).every(isEmptyBlock) &&\n                Math.abs(\n                    ev.clientX -\n                        (this._lastMousedownPosition ? this._lastMousedownPosition[0] : ev.clientX)\n                ) >= 20\n            ) {\n                // Handle selecting an empty cell.\n                this.selectTableCells(selection);\n            }\n        }\n    }\n\n    selectTableCells(selection) {\n        const table = closestElement(selection.commonAncestorContainer, \"table\");\n        table.classList.toggle(\"o_selected_table\", true);\n        const columns = [...table.querySelectorAll(\"td\")].filter(\n            (td) => closestElement(td, \"table\") === table\n        );\n        const startCol =\n            [selection.startContainer, ...ancestors(selection.startContainer, this.editable)].find(\n                (node) => node.nodeName === \"TD\" && closestElement(node, \"table\") === table\n            ) || columns[0];\n        const endCol =\n            [selection.endContainer, ...ancestors(selection.endContainer, this.editable)].find(\n                (node) => node.nodeName === \"TD\" && closestElement(node, \"table\") === table\n            ) || columns[columns.length - 1];\n        const [startRow, endRow] = [closestElement(startCol, \"tr\"), closestElement(endCol, \"tr\")];\n        const [startColIndex, endColIndex] = [getColumnIndex(startCol), getColumnIndex(endCol)];\n        const [startRowIndex, endRowIndex] = [getRowIndex(startRow), getRowIndex(endRow)];\n        const [minRowIndex, maxRowIndex] = [\n            Math.min(startRowIndex, endRowIndex),\n            Math.max(startRowIndex, endRowIndex),\n        ];\n        const [minColIndex, maxColIndex] = [\n            Math.min(startColIndex, endColIndex),\n            Math.max(startColIndex, endColIndex),\n        ];\n        // Create an array of arrays of tds (each of which is a row).\n        const grid = [...table.querySelectorAll(\"tr\")]\n            .filter((tr) => closestElement(tr, \"table\") === table)\n            .map((tr) => [...tr.children].filter((child) => child.nodeName === \"TD\"));\n        for (const tds of grid.filter((_, index) => index >= minRowIndex && index <= maxRowIndex)) {\n            for (const td of tds.filter(\n                (_, index) => index >= minColIndex && index <= maxColIndex\n            )) {\n                td.classList.toggle(\"o_selected_td\", true);\n            }\n        }\n    }\n\n    /**\n     * Remove any custom table selection from the editor.\n     *\n     * @returns {boolean} true if a table was deselected\n     */\n    deselectTable(root = this.editable) {\n        let didDeselectTable = false;\n        for (const table of root.querySelectorAll(\".o_selected_table\")) {\n            removeClass(table, \"o_selected_table\");\n            for (const td of table.querySelectorAll(\".o_selected_td\")) {\n                removeClass(td, \"o_selected_td\");\n            }\n            didDeselectTable = true;\n        }\n        return didDeselectTable;\n    }\n\n    applyTableColor(color, mode, previewMode) {\n        const selectedTds = [...this.editable.querySelectorAll(\"td.o_selected_td\")].filter(\n            (node) => node.isContentEditable\n        );\n        if (selectedTds.length && mode === \"backgroundColor\") {\n            if (previewMode) {\n                // Temporarily remove backgroundColor applied by \"o_selected_td\" class with !important.\n                selectedTds.forEach((td) => td.classList.remove(\"o_selected_td\"));\n            }\n            for (const td of selectedTds) {\n                this.dependencies.color.colorElement(td, color, mode);\n                if (color) {\n                    td.style[\"color\"] = getComputedStyle(td).color;\n                } else {\n                    td.style[\"color\"] = \"\";\n                }\n            }\n        }\n    }\n\n    adjustTraversedNodes(traversedNodes) {\n        const modifiedTraversedNodes = [];\n        const visitedTables = new Set();\n        for (const node of traversedNodes) {\n            const selectedTable = closestElement(node, \".o_selected_table\");\n            if (selectedTable) {\n                if (visitedTables.has(selectedTable)) {\n                    continue;\n                }\n                visitedTables.add(selectedTable);\n                for (const selectedTd of selectedTable.querySelectorAll(\".o_selected_td\")) {\n                    modifiedTraversedNodes.push(selectedTd, ...descendants(selectedTd));\n                }\n            } else {\n                modifiedTraversedNodes.push(node);\n            }\n        }\n        return modifiedTraversedNodes;\n    }\n\n    resetTableSelection() {\n        const selection = this.dependencies.selection.getEditableSelection({ deep: true });\n        const anchorTD = closestElement(selection.anchorNode, \".o_selected_td\");\n        if (!anchorTD) {\n            return;\n        }\n        this.deselectTable();\n        this.dependencies.selection.setSelection({\n            anchorNode: anchorTD.firstChild,\n            anchorOffset: 0,\n            focusNode: anchorTD.lastChild,\n            focusOffset: nodeSize(anchorTD.lastChild),\n        });\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport {\n    closestElement,\n    getAdjacentNextSiblings,\n    getAdjacentPreviousSiblings,\n} from \"@html_editor/utils/dom_traversal\";\nimport { getColumnIndex } from \"@html_editor/utils/table\";\nimport { BORDER_SENSITIVITY } from \"@html_editor/main/table/table_plugin\";\n\nexport class TableResizePlugin extends Plugin {\n    static id = \"tableResize\";\n    static dependencies = [\"table\", \"history\"];\n\n    setup() {\n        this.addDomListener(this.editable, \"mousedown\", this.onMousedown);\n        this.addDomListener(this.editable, \"mousemove\", this.onMousemove);\n    }\n\n    /**\n     * If the mouse is hovering over one of the borders of a table cell element,\n     * return the side of that border ('left'|'top'|'right'|'bottom').\n     * Otherwise, return false.\n     *\n     * @private\n     * @param {MouseEvent} ev\n     * @returns {string|boolean}\n     */\n    isHoveringTdBorder(ev) {\n        const target = /** @type {HTMLElement} */ (ev.target);\n        if (ev.target && target.nodeName === \"TD\" && target.isContentEditable) {\n            const targetRect = target.getBoundingClientRect();\n            if (ev.clientX <= targetRect.x + BORDER_SENSITIVITY) {\n                return \"left\";\n            } else if (ev.clientY <= targetRect.y + BORDER_SENSITIVITY) {\n                return \"top\";\n            } else if (ev.clientX >= targetRect.x + target.clientWidth - BORDER_SENSITIVITY) {\n                return \"right\";\n            } else if (ev.clientY >= targetRect.y + target.clientHeight - BORDER_SENSITIVITY) {\n                return \"bottom\";\n            }\n        }\n        return false;\n    }\n    /**\n     * Change the cursor to a resizing cursor, in the direction specified. If no\n     * direction is specified, return the cursor to its default.\n     *\n     * @private\n     * @param {'col'|'row'|false} direction 'col'/'row' to hint column/row,\n     *                                      false to remove the hints\n     */\n    setTableResizeCursor(direction) {\n        const classList = this.editable.classList;\n        if (classList.contains(\"o_col_resize\")) {\n            classList.remove(\"o_col_resize\");\n        }\n        if (classList.contains(\"o_row_resize\")) {\n            classList.remove(\"o_row_resize\");\n        }\n        if (direction === \"col\") {\n            this.editable.classList.add(\"o_col_resize\");\n        } else if (direction === \"row\") {\n            this.editable.classList.add(\"o_row_resize\");\n        }\n    }\n\n    /**\n     * Resizes a table in the given direction, by \"pulling\" the border between\n     * the given targets (ordered left to right or top to bottom).\n     *\n     * @param {MouseEvent} ev\n     * @param {'col'|'row'} direction\n     * @param {HTMLElement} target1\n     * @param {HTMLElement} target2\n     */\n    resizeTable(ev, direction, target1, target2) {\n        ev.preventDefault();\n        const position = target1 ? (target2 ? \"middle\" : \"last\") : \"first\";\n        let [item, neighbor] = [target1 || target2, target2];\n        const table = closestElement(item, \"table\");\n        const [sizeProp, positionProp, clientPositionProp] =\n            direction === \"col\" ? [\"width\", \"x\", \"clientX\"] : [\"height\", \"y\", \"clientY\"];\n\n        const isRTL = this.config.direction === \"rtl\";\n        // Preserve current width.\n        if (sizeProp === \"width\") {\n            const tableRect = table.getBoundingClientRect();\n            table.style[sizeProp] = tableRect[sizeProp] + \"px\";\n        }\n        const unsizedItemsSelector = `${\n            direction === \"col\" ? \"td\" : \"tr\"\n        }:not([style*=${sizeProp}])`;\n        for (const unsizedItem of table.querySelectorAll(unsizedItemsSelector)) {\n            unsizedItem.style[sizeProp] = unsizedItem.getBoundingClientRect()[sizeProp] + \"px\";\n        }\n\n        // TD widths should only be applied in the first row. Change targets and\n        // clean the rest.\n        if (direction === \"col\") {\n            let hostCell = closestElement(table, \"td\");\n            const hostCells = [];\n            while (hostCell) {\n                hostCells.push(hostCell);\n                hostCell = closestElement(hostCell.parentElement, \"td\");\n            }\n            const nthColumn = getColumnIndex(item);\n            const firstRow = [...table.querySelector(\"tr\").children];\n            [item, neighbor] = [firstRow[nthColumn], firstRow[nthColumn + 1]];\n            for (const td of hostCells) {\n                if (\n                    td !== item &&\n                    td !== neighbor &&\n                    closestElement(td, \"table\") === table &&\n                    getColumnIndex(td) !== 0\n                ) {\n                    td.style.removeProperty(sizeProp);\n                }\n            }\n            if (isRTL && position == \"middle\") {\n                [item, neighbor] = [neighbor, item];\n            }\n        }\n\n        const MIN_SIZE = 33; // TODO: ideally, find this value programmatically.\n        switch (position) {\n            case \"first\": {\n                const marginProp =\n                    direction === \"col\" ? (isRTL ? \"marginRight\" : \"marginLeft\") : \"marginTop\";\n                const itemRect = item.getBoundingClientRect();\n                const tableStyle = getComputedStyle(table);\n                const currentMargin = parseFloat(tableStyle[marginProp]);\n                let sizeDelta = itemRect[positionProp] - ev[clientPositionProp];\n                if (direction === \"col\" && isRTL) {\n                    sizeDelta =\n                        ev[clientPositionProp] - itemRect[positionProp] - itemRect[sizeProp];\n                }\n                const newMargin = currentMargin - sizeDelta;\n                const currentSize = itemRect[sizeProp];\n                const newSize = currentSize + sizeDelta;\n                if (newMargin >= 0 && newSize > MIN_SIZE) {\n                    const tableRect = table.getBoundingClientRect();\n                    // Check if a nested table would overflow its parent cell.\n                    const hostCell = closestElement(table.parentElement, \"td\");\n                    const childTable = item.querySelector(\"table\");\n                    const endProp = isRTL ? \"left\" : \"right\";\n                    if (\n                        direction === \"col\" &&\n                        ((hostCell &&\n                            tableRect[endProp] + sizeDelta >\n                                hostCell.getBoundingClientRect()[endProp] - 5) ||\n                            (childTable &&\n                                childTable.getBoundingClientRect()[endProp] >\n                                    itemRect[endProp] + sizeDelta - 5))\n                    ) {\n                        break;\n                    }\n                    table.style[marginProp] = newMargin + \"px\";\n                    item.style[sizeProp] = newSize + \"px\";\n                    if (sizeProp === \"width\") {\n                        table.style[sizeProp] = tableRect[sizeProp] + sizeDelta + \"px\";\n                    }\n                }\n                break;\n            }\n            case \"middle\": {\n                const [itemRect, neighborRect] = [\n                    item.getBoundingClientRect(),\n                    neighbor.getBoundingClientRect(),\n                ];\n                const [currentSize, newSize] = [\n                    itemRect[sizeProp],\n                    ev[clientPositionProp] - itemRect[positionProp],\n                ];\n                const editableStyle = getComputedStyle(this.editable);\n                const sizeDelta = newSize - currentSize;\n                const currentNeighborSize = neighborRect[sizeProp];\n                const newNeighborSize = currentNeighborSize - sizeDelta;\n                const maxWidth =\n                    this.editable.clientWidth -\n                    parseFloat(editableStyle.paddingLeft) -\n                    parseFloat(editableStyle.paddingRight);\n                const tableRect = table.getBoundingClientRect();\n                if (\n                    newSize > MIN_SIZE &&\n                    // prevent resizing horizontally beyond the bounds of\n                    // the editable:\n                    (direction === \"row\" ||\n                        newNeighborSize > MIN_SIZE ||\n                        tableRect[sizeProp] + sizeDelta < maxWidth)\n                ) {\n                    // Check if a nested table would overflow its parent cell.\n                    const childTable = item.querySelector(\"table\");\n                    if (\n                        direction === \"col\" &&\n                        childTable &&\n                        childTable.getBoundingClientRect().right > itemRect.right + sizeDelta - 5\n                    ) {\n                        break;\n                    }\n                    item.style[sizeProp] = newSize + \"px\";\n                    if (direction === \"col\") {\n                        neighbor.style[sizeProp] =\n                            (newNeighborSize > MIN_SIZE ? newNeighborSize : currentNeighborSize) +\n                            \"px\";\n                    } else if (sizeProp === \"width\") {\n                        table.style[sizeProp] = tableRect[sizeProp] + sizeDelta + \"px\";\n                    }\n                }\n                break;\n            }\n            case \"last\": {\n                const itemRect = item.getBoundingClientRect();\n                let sizeDelta =\n                    ev[clientPositionProp] - (itemRect[positionProp] + itemRect[sizeProp]); // todo: rephrase\n                if (direction === \"col\" && isRTL) {\n                    sizeDelta = itemRect[positionProp] - ev[clientPositionProp];\n                }\n                const currentSize = itemRect[sizeProp];\n                const newSize = currentSize + sizeDelta;\n                if ((newSize >= 0 || direction === \"row\") && newSize > MIN_SIZE) {\n                    const tableRect = table.getBoundingClientRect();\n                    // Check if a nested table would overflow its parent cell.\n                    const hostCell = closestElement(table.parentElement, \"td\");\n                    const childTable = item.querySelector(\"table\");\n                    const endProp = isRTL ? \"left\" : \"right\";\n                    if (\n                        direction === \"col\" &&\n                        ((hostCell &&\n                            tableRect[endProp] + sizeDelta >\n                                hostCell.getBoundingClientRect()[endProp] - 5) ||\n                            (childTable &&\n                                childTable.getBoundingClientRect()[endProp] >\n                                    itemRect[endProp] + sizeDelta - 5))\n                    ) {\n                        break;\n                    }\n                    if (sizeProp === \"width\") {\n                        table.style[sizeProp] = tableRect[sizeProp] + sizeDelta + \"px\";\n                    }\n                    item.style[sizeProp] = newSize + \"px\";\n                }\n                break;\n            }\n        }\n    }\n\n    onMousedown(ev) {\n        const isHoveringTdBorder = this.isHoveringTdBorder(ev);\n        const isRTL = this.config.direction === \"rtl\";\n        if (isHoveringTdBorder) {\n            ev.preventDefault();\n            const direction =\n                { top: \"row\", right: \"col\", bottom: \"row\", left: \"col\" }[isHoveringTdBorder] ||\n                false;\n            let target1, target2;\n            const column = closestElement(ev.target, \"tr\");\n            if (isHoveringTdBorder === \"top\" && column) {\n                target1 = getAdjacentPreviousSiblings(column).find(\n                    (node) => node.nodeName === \"TR\"\n                );\n                target2 = closestElement(ev.target, \"tr\");\n            } else if (isHoveringTdBorder === \"right\") {\n                if (isRTL) {\n                    target1 = getAdjacentPreviousSiblings(ev.target).find(\n                        (node) => node.nodeName === \"TD\"\n                    );\n                    target2 = ev.target;\n                } else {\n                    target1 = ev.target;\n                    target2 = getAdjacentNextSiblings(ev.target).find(\n                        (node) => node.nodeName === \"TD\"\n                    );\n                }\n            } else if (isHoveringTdBorder === \"bottom\" && column) {\n                target1 = closestElement(ev.target, \"tr\");\n                target2 = getAdjacentNextSiblings(column).find((node) => node.nodeName === \"TR\");\n            } else if (isHoveringTdBorder === \"left\") {\n                if (isRTL) {\n                    target1 = ev.target;\n                    target2 = getAdjacentNextSiblings(ev.target).find(\n                        (node) => node.nodeName === \"TD\"\n                    );\n                } else {\n                    target1 = getAdjacentPreviousSiblings(ev.target).find(\n                        (node) => node.nodeName === \"TD\"\n                    );\n                    target2 = ev.target;\n                }\n            }\n            this.isResizingTable = true;\n            this.setTableResizeCursor(direction);\n            const resizeTable = (ev) => this.resizeTable(ev, direction, target1, target2);\n            const stopResizing = (ev) => {\n                ev.preventDefault();\n                this.isResizingTable = false;\n                this.setTableResizeCursor(false);\n                this.dependencies.history.addStep();\n                this.document.removeEventListener(\"mousemove\", resizeTable);\n                this.document.removeEventListener(\"mouseup\", stopResizing);\n                this.document.removeEventListener(\"mouseleave\", stopResizing);\n            };\n            this.document.addEventListener(\"mousemove\", resizeTable);\n            this.document.addEventListener(\"mouseup\", stopResizing);\n            this.document.addEventListener(\"mouseleave\", stopResizing);\n        }\n    }\n    onMousemove(ev) {\n        const direction =\n            { top: \"row\", right: \"col\", bottom: \"row\", left: \"col\" }[this.isHoveringTdBorder(ev)] ||\n            false;\n        if (direction || !this.isResizingTable) {\n            this.setTableResizeCursor(direction);\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { reactive } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { TableMenu } from \"./table_menu\";\nimport { TablePicker } from \"./table_picker\";\n\n/**\n * This plugin only contains the table ui feature (table picker, menus, ...).\n * All actual table manipulation code is located in the table plugin.\n */\nexport class TableUIPlugin extends Plugin {\n    static id = \"tableUi\";\n    static dependencies = [\"history\", \"overlay\", \"table\"];\n    resources = {\n        user_commands: [\n            {\n                id: \"openTablePicker\",\n                title: _t(\"Table\"),\n                description: _t(\"Insert a table\"),\n                icon: \"fa-table\",\n                run: this.openPickerOrInsertTable.bind(this),\n            },\n        ],\n        powerbox_items: [\n            {\n                categoryId: \"structure\",\n                commandId: \"openTablePicker\",\n            },\n        ],\n        power_buttons: { commandId: \"openTablePicker\" },\n    };\n\n    setup() {\n        /** @type {import(\"@html_editor/core/overlay_plugin\").Overlay} */\n        this.picker = this.dependencies.overlay.createOverlay(TablePicker, {\n            positionOptions: {\n                updatePositionOnResize: false,\n                onPositioned: (picker, position) => {\n                    const popperRect = picker.getBoundingClientRect();\n                    const { left } = position;\n                    if (this.config.direction === \"rtl\") {\n                        // position from the right instead of the left as it is needed\n                        // to ensure the expand animation is properly done\n                        picker.style.right = `${window.innerWidth - left - popperRect.width}px`;\n                        picker.style.removeProperty(\"left\");\n                    }\n                },\n            },\n        });\n\n        this.activeTd = null;\n\n        /** @type {import(\"@html_editor/core/overlay_plugin\").Overlay} */\n        this.colMenu = this.dependencies.overlay.createOverlay(TableMenu, {\n            positionOptions: {\n                position: \"top-fit\",\n                onPositioned: (el, solution) => {\n                    // Only accept top position as solution.\n                    if (solution.direction !== \"top\") {\n                        el.style.display = \"none\"; // avoid glitch\n                        this.colMenu.close();\n                    }\n                },\n            },\n        });\n        /** @type {import(\"@html_editor/core/overlay_plugin\").Overlay} */\n        this.rowMenu = this.dependencies.overlay.createOverlay(TableMenu, {\n            positionOptions: {\n                position: \"left-fit\",\n            },\n        });\n        this.addDomListener(this.document, \"pointermove\", this.onMouseMove);\n        const closeMenus = () => {\n            if (this.isMenuOpened) {\n                this.isMenuOpened = false;\n                this.colMenu.close();\n                this.rowMenu.close();\n            }\n        };\n        this.addDomListener(this.document, \"scroll\", closeMenus, true);\n    }\n\n    openPicker() {\n        this.picker.open({\n            props: {\n                editable: this.editable,\n                overlay: this.picker,\n                direction: this.config.direction || \"ltr\",\n                insertTable: (params) => this.dependencies.table.insertTable(params),\n            },\n        });\n    }\n\n    openPickerOrInsertTable() {\n        if (this.services.ui.isSmall) {\n            this.dependencies.table.insertTable({ cols: 3, rows: 3 });\n        } else {\n            this.openPicker();\n        }\n    }\n\n    onMouseMove(ev) {\n        const target = ev.target;\n        if (this.isMenuOpened) {\n            return;\n        }\n        if (\n            [\"TD\", \"TH\"].includes(target.tagName) &&\n            target !== this.activeTd &&\n            this.editable.contains(target)\n        ) {\n            if (ev.target.isContentEditable) {\n                this.setActiveTd(target);\n            }\n        } else if (this.activeTd) {\n            const isOverlay = target.closest(\".o-overlay-container\");\n            if (isOverlay) {\n                return;\n            }\n            const parentTd = closestElement(target, \"td, th\");\n            if (!parentTd) {\n                this.setActiveTd(null);\n            }\n        }\n    }\n\n    createDropdownState(menuToClose) {\n        const dropdownState = reactive({\n            isOpen: false,\n            open: () => {\n                dropdownState.isOpen = true;\n                menuToClose.close();\n                this.isMenuOpened = true;\n            },\n            close: () => {\n                dropdownState.isOpen = false;\n                this.isMenuOpened = false;\n            },\n        });\n        return dropdownState;\n    }\n\n    setActiveTd(td) {\n        this.activeTd = td;\n        this.colMenu.close();\n        this.rowMenu.close();\n        if (!td) {\n            return;\n        }\n        const withAddStep = (fn) => {\n            return (...args) => {\n                fn(...args);\n                this.dependencies.history.addStep();\n            };\n        };\n        const tableMethods = {\n            moveColumn: withAddStep(this.dependencies.table.moveColumn),\n            addColumn: withAddStep(this.dependencies.table.addColumn),\n            removeColumn: withAddStep(this.dependencies.table.removeColumn),\n            moveRow: withAddStep(this.dependencies.table.moveRow),\n            addRow: withAddStep(this.dependencies.table.addRow),\n            removeRow: withAddStep(this.dependencies.table.removeRow),\n            resetTableSize: withAddStep(this.dependencies.table.resetTableSize),\n        };\n        if (td.cellIndex === 0) {\n            this.rowMenu.open({\n                target: td,\n                props: {\n                    type: \"row\",\n                    overlay: this.rowMenu,\n                    target: td,\n                    dropdownState: this.createDropdownState(this.colMenu),\n                    ...tableMethods,\n                },\n            });\n        }\n        if (td.parentElement.rowIndex === 0) {\n            this.colMenu.open({\n                target: td,\n                props: {\n                    type: \"column\",\n                    overlay: this.colMenu,\n                    target: td,\n                    dropdownState: this.createDropdownState(this.rowMenu),\n                    direction: this.config.direction || \"ltr\",\n                    ...tableMethods,\n                },\n            });\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { closestBlock } from \"@html_editor/utils/blocks\";\nimport { splitTextNode } from \"@html_editor/utils/dom\";\nimport { isEditorTab, isTextNode, isZWS } from \"@html_editor/utils/dom_info\";\nimport { descendants, getAdjacentPreviousSiblings } from \"@html_editor/utils/dom_traversal\";\nimport { parseHTML } from \"@html_editor/utils/html\";\nimport { DIRECTIONS, childNodeIndex } from \"@html_editor/utils/position\";\n\nconst tabHtml = '<span class=\"oe-tabs\" contenteditable=\"false\">\\u0009</span>\\u200B';\nconst GRID_COLUMN_WIDTH = 40; //@todo Configurable?\n\n/**\n * Checks if the given tab element represents an indentation.\n * An indentation tab is one that is not preceded by visible text.\n *\n * @param {HTMLElement} tab - The tab element to check.\n * @returns {boolean} - True if the tab represents an indentation, false otherwise.\n */\nfunction isIndentationTab(tab) {\n    return !getAdjacentPreviousSiblings(tab).some(\n        (sibling) => isTextNode(sibling) && !/^[\\u200B\\s]*$/.test(sibling.textContent)\n    );\n}\n\n/**\n * @typedef { Object } TabulationShared\n * @property { TabulationPlugin['indentBlocks'] } indentBlocks\n * @property { TabulationPlugin['outdentBlocks'] } outdentBlocks\n */\n\nexport class TabulationPlugin extends Plugin {\n    static id = \"tabulation\";\n    static dependencies = [\"dom\", \"selection\", \"history\", \"delete\"];\n    static shared = [\"indentBlocks\", \"outdentBlocks\"];\n    resources = {\n        user_commands: [\n            { id: \"tab\", run: this.handleTab.bind(this) },\n            { id: \"shiftTab\", run: this.handleShiftTab.bind(this) },\n        ],\n        shortcuts: [\n            { hotkey: \"tab\", commandId: \"tab\" },\n            { hotkey: \"shift+tab\", commandId: \"shiftTab\" },\n        ],\n\n        /** Handlers */\n        clean_for_save_handlers: ({ root }) => {\n            for (const tab of root.querySelectorAll(\"span.oe-tabs\")) {\n                tab.removeAttribute(\"contenteditable\");\n            }\n        },\n        normalize_handlers: this.normalize.bind(this),\n\n        /** Overrides */\n        delete_forward_overrides: this.handleDeleteForward.bind(this),\n\n        unsplittable_node_predicates: isEditorTab, // avoid merge\n    };\n\n    handleTab() {\n        if (this.delegateTo(\"tab_overrides\")) {\n            return;\n        }\n\n        const selection = this.dependencies.selection.getEditableSelection();\n        if (selection.isCollapsed) {\n            this.insertTab();\n        } else {\n            const traversedBlocks = this.dependencies.selection.getTraversedBlocks();\n            this.indentBlocks(traversedBlocks);\n        }\n        this.dependencies.history.addStep();\n    }\n\n    handleShiftTab() {\n        if (this.delegateTo(\"shift_tab_overrides\")) {\n            return;\n        }\n        const traversedBlocks = this.dependencies.selection.getTraversedBlocks();\n        this.outdentBlocks(traversedBlocks);\n        this.dependencies.history.addStep();\n    }\n\n    insertTab() {\n        this.dependencies.dom.insert(parseHTML(this.document, tabHtml));\n    }\n\n    /**\n     * @param {HTMLElement} blocks\n     */\n    indentBlocks(blocks) {\n        const selectionToRestore = this.dependencies.selection.getEditableSelection();\n        const tab = parseHTML(this.document, tabHtml);\n        for (const block of blocks) {\n            block.prepend(tab.cloneNode(true));\n        }\n        this.dependencies.selection.setSelection(selectionToRestore, { normalize: false });\n    }\n\n    /**\n     * @param {HTMLElement} blocks\n     */\n    outdentBlocks(blocks) {\n        for (const block of blocks) {\n            const firstTab = descendants(block).find(isEditorTab);\n            if (firstTab && isIndentationTab(firstTab)) {\n                this.removeTrailingZWS(firstTab);\n                firstTab.remove();\n            }\n        }\n    }\n\n    removeTrailingZWS(tab) {\n        const selection = this.dependencies.selection.getEditableSelection();\n        const { anchorNode, anchorOffset, focusNode, focusOffset } = selection;\n        const updateAnchor = anchorNode === tab.nextSibling;\n        const updateFocus = focusNode === tab.nextSibling;\n        let zwsRemoved = 0;\n        while (\n            tab.nextSibling &&\n            tab.nextSibling.nodeType === Node.TEXT_NODE &&\n            tab.nextSibling.textContent.startsWith(\"\\u200B\")\n        ) {\n            splitTextNode(tab.nextSibling, 1, DIRECTIONS.LEFT);\n            tab.nextSibling.remove();\n            zwsRemoved++;\n        }\n        if (updateAnchor || updateFocus) {\n            this.dependencies.selection.setSelection({\n                anchorNode: updateAnchor ? tab.nextSibling : anchorNode,\n                anchorOffset: updateAnchor ? Math.max(0, anchorOffset - zwsRemoved) : anchorOffset,\n                focusNode: updateFocus ? tab.nextSibling : focusNode,\n                focusOffset: updateFocus ? Math.max(0, focusOffset - zwsRemoved) : focusOffset,\n            });\n        }\n    }\n\n    /**\n     * @param {HTMLSpanElement} tabSpan - span.oe-tabs element\n     */\n    adjustTabWidth(tabSpan) {\n        let tabPreviousSibling = tabSpan.previousSibling;\n        while (isZWS(tabPreviousSibling)) {\n            tabPreviousSibling = tabPreviousSibling.previousSibling;\n        }\n        if (isEditorTab(tabPreviousSibling)) {\n            tabSpan.style.width = `${GRID_COLUMN_WIDTH}px`;\n            return;\n        }\n        const spanRect = tabSpan.getBoundingClientRect();\n        const referenceRect = this.editable.firstElementChild?.getBoundingClientRect();\n        // @ todo @phoenix Re-evaluate if this check is necessary.\n        // Values from getBoundingClientRect() are all zeros during\n        // Editor startup or saving. We cannot recalculate the tabs\n        // width in thoses cases.\n        if (!referenceRect?.width || !spanRect.width) {\n            return;\n        }\n        const relativePosition = spanRect.left - referenceRect.left;\n        const distToNextGridLine = GRID_COLUMN_WIDTH - (relativePosition % GRID_COLUMN_WIDTH);\n        // Round to the first decimal point.\n        const width = distToNextGridLine.toFixed(1);\n        tabSpan.style.width = `${width}px`;\n    }\n\n    /**\n     * Aligns the tabs under the specified tree to a grid.\n     *\n     * @param {HTMLElement} [root] - The tree root.\n     */\n    alignTabs(root = this.editable) {\n        const block = closestBlock(root);\n        if (!block) {\n            return;\n        }\n        for (const tab of block.querySelectorAll(\"span.oe-tabs\")) {\n            this.adjustTabWidth(tab);\n        }\n    }\n\n    // When deleting an editor tab, we need to ensure it's related\n    // ZWS will deleted as well.\n    // @todo @phoenix: for some reason, there might be more than one ZWS.\n    // Investigate why.\n    expandRangeToIncludeZWS(tabElement) {\n        let previous = tabElement;\n        let node = tabElement.nextSibling;\n        while (node?.nodeType === Node.TEXT_NODE) {\n            for (let i = 0; i < node.textContent.length; i++) {\n                if (node.textContent[i] !== \"\\u200B\") {\n                    return [node, i];\n                }\n            }\n            previous = node;\n            node = node.nextSibling;\n        }\n        return [previous.parentElement, childNodeIndex(previous) + 1];\n    }\n\n    // @todo consider registering this as adjustRange callback instead.\n    handleDeleteForward(range) {\n        let { endContainer, endOffset } = range;\n        if (!(endContainer?.nodeType === Node.ELEMENT_NODE) || !endOffset) {\n            return;\n        }\n        const nodeToDelete = endContainer.childNodes[endOffset - 1];\n        if (isEditorTab(nodeToDelete)) {\n            [endContainer, endOffset] = this.expandRangeToIncludeZWS(nodeToDelete);\n            range = this.dependencies.delete.deleteRange({ ...range, endContainer, endOffset });\n            this.dependencies.selection.setSelection({\n                anchorNode: range.startContainer,\n                anchorOffset: range.startOffset,\n            });\n            return true;\n        }\n    }\n    normalize(el) {\n        for (const tab of el.querySelectorAll(\".oe-tabs\")) {\n            tab.setAttribute(\"contenteditable\", \"false\");\n        }\n        this.alignTabs(el);\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Plugin } from \"../plugin\";\nimport { closestBlock } from \"../utils/blocks\";\nimport { closestElement } from \"../utils/dom_traversal\";\nimport { isContentEditable, isTextNode } from \"@html_editor/utils/dom_info\";\n\nexport class TextDirectionPlugin extends Plugin {\n    static id = \"textDirection\";\n    static dependencies = [\"selection\", \"history\", \"split\", \"format\"];\n    resources = {\n        user_commands: [\n            {\n                id: \"switchDirection\",\n                title: _t(\"Switch direction\"),\n                description: _t(\"Switch the text's direction\"),\n                icon: \"fa-exchange\",\n                run: this.switchDirection.bind(this),\n            },\n        ],\n        powerbox_items: [\n            {\n                categoryId: \"format\",\n                commandId: \"switchDirection\",\n            },\n        ],\n    };\n\n    setup() {\n        if (this.config.direction) {\n            this.editable.setAttribute(\"dir\", this.config.direction);\n        }\n        this.direction = this.config.direction || \"ltr\";\n    }\n\n    switchDirection() {\n        const selection = this.dependencies.split.splitSelection();\n        const selectedTextNodes = [\n            selection.anchorNode,\n            ...this.dependencies.selection.getSelectedNodes(),\n        ].filter((n) => isTextNode(n) && isContentEditable(n) && n.nodeValue.trim().length);\n        const blocks = new Set(\n            selectedTextNodes.map(\n                (textNode) => closestElement(textNode, \"ul,ol\") || closestBlock(textNode)\n            )\n        );\n\n        const shouldApplyStyle = !this.dependencies.format.isSelectionFormat(\"switchDirection\");\n\n        for (const block of blocks) {\n            for (const node of block.querySelectorAll(\"ul,ol\")) {\n                blocks.add(node);\n            }\n        }\n        for (const block of blocks) {\n            if (!shouldApplyStyle) {\n                block.removeAttribute(\"dir\");\n            } else {\n                block.setAttribute(\"dir\", this.direction === \"ltr\" ? \"rtl\" : \"ltr\");\n            }\n        }\n\n        for (const element of blocks) {\n            const style = getComputedStyle(element);\n            if (style.direction === \"ltr\" && style.textAlign === \"right\") {\n                element.style.setProperty(\"text-align\", \"left\");\n            } else if (style.direction === \"rtl\" && style.textAlign === \"left\") {\n                element.style.setProperty(\"text-align\", \"right\");\n            }\n        }\n        this.dependencies.history.addStep();\n    }\n}\n", "import { Component, onMounted, useExternalListener, useRef } from \"@odoo/owl\";\nimport { Toolbar } from \"./toolbar\";\n\nexport class ToolbarMobile extends Component {\n    static template = \"html_editor.MobileToolbar\";\n    static props = [\"*\"];\n    static components = {\n        Toolbar,\n    };\n\n    setup() {\n        this.toolbar = useRef(\"toolbarWrapper\");\n        useExternalListener(window.visualViewport, \"resize\", this.fixToolbarPosition);\n        useExternalListener(window.visualViewport, \"scroll\", this.fixToolbarPosition);\n        onMounted(() => {\n            this.fixToolbarPosition();\n        });\n    }\n\n    /**\n     * Fixes the position of the toolbar for the keyboard height.\n     */\n    fixToolbarPosition() {\n        const keyboardHeight =\n            window.innerHeight - (window.visualViewport.height + window.visualViewport.offsetTop);\n        if (keyboardHeight > 0) {\n            this.toolbar.el.style.bottom = `${keyboardHeight}px`;\n        } else {\n            this.toolbar.el.style.bottom = `0px`;\n        }\n    }\n}\n", "import { Component, useState, validate } from \"@odoo/owl\";\n\nexport class Toolbar extends Component {\n    static template = \"html_editor.Toolbar\";\n    static props = {\n        class: { type: String, optional: true },\n        toolbar: {\n            type: Object,\n            shape: {\n                getSelection: Function,\n                focusEditable: Function,\n                buttonGroups: {\n                    type: Array,\n                    element: {\n                        type: Object,\n                        shape: {\n                            id: String,\n                            namespace: { type: String, optional: true },\n                            buttons: {\n                                type: Array,\n                                element: {\n                                    type: Object,\n                                    validate: (button) => {\n                                        const base = {\n                                            id: String,\n                                            groupId: String,\n                                            title: String,\n                                            isAvailable: { type: Function, optional: true },\n                                            isDisabled: { type: Function, optional: true },\n                                        };\n                                        if (button.Component) {\n                                            validate(button, {\n                                                ...base,\n                                                Component: Function,\n                                                props: { type: Object, optional: true },\n                                            });\n                                        } else {\n                                            validate(button, {\n                                                ...base,\n                                                run: Function,\n                                                icon: { type: String, optional: true },\n                                                text: { type: String, optional: true },\n                                                isActive: { type: Function, optional: true },\n                                            });\n                                        }\n                                        return true;\n                                    },\n                                },\n                            },\n                        },\n                    },\n                },\n                state: {\n                    type: Object,\n                    shape: {\n                        buttonsActiveState: Object,\n                        buttonsDisabledState: Object,\n                        buttonsAvailableState: Object,\n                        namespace: {\n                            type: String,\n                            optional: true,\n                        },\n                    },\n                },\n            },\n        },\n    };\n\n    setup() {\n        this.state = useState(this.props.toolbar.state);\n    }\n\n    getFilteredButtonGroups() {\n        if (this.state.namespace) {\n            const filteredGroups = this.props.toolbar.buttonGroups.filter(\n                (group) => group.namespace === this.state.namespace\n            );\n            if (filteredGroups.length > 0) {\n                return filteredGroups;\n            }\n        }\n        return this.props.toolbar.buttonGroups.filter((group) => group.namespace === undefined);\n    }\n\n    onButtonClick(button) {\n        button.run();\n        this.props.toolbar.focusEditable();\n    }\n}\n\nexport const toolbarButtonProps = {\n    title: String,\n    getSelection: Function,\n};\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { isZWS } from \"@html_editor/utils/dom_info\";\nimport { reactive } from \"@odoo/owl\";\nimport { isTextNode } from \"@web/views/view_compiler\";\nimport { Toolbar } from \"./toolbar\";\nimport { hasTouch } from \"@web/core/browser/feature_detection\";\nimport { registry } from \"@web/core/registry\";\nimport { ToolbarMobile } from \"./mobile_toolbar\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { omit, pick } from \"@web/core/utils/objects\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\n\n/** @typedef { import(\"@html_editor/core/selection_plugin\").EditorSelection } EditorSelection */\n/** @typedef { import(\"@html_editor/core/user_command_plugin\").UserCommand } UserCommand */\n/** @typedef { import(\"@web/core/l10n/translation.js\")._t} _t */\n/** @typedef { ReturnType<_t> } TranslatedString */\n\n/**\n * @typedef {Object} ToolbarNamespace\n * @property {string} id\n * @property {(traversedNodes: Node[]) => boolean} isApplied\n *\n *\n * @typedef {Object} ToolbarGroup\n * @property {string} id\n * @property {string} [namespace]\n *\n *\n * @typedef {ToolbarCommandItem | ToolbarComponentItem} ToolbarItem\n *\n * @typedef {Object} ToolbarCommandItem\n * Regular button: derives from a user command specified by commandId.\n * The properties maked with * can be omitted if they are present in the user command.\n * The ones marked with ?* are both optional and derivable from the user command.\n * @property {string} id\n * @property {string} groupId Id of a toolbar group\n * @property {string} commandId\n * @property {Object} [commandParams] Passed to the command's `run` function\n * @property {TranslatedString} [title] * - becomes the button's title (and tooltip content)\n * @property {string} [icon] *\n * @property {string} [text] Can be used with (or instead of) `icon`\n * @property {(selection: EditorSelection) => boolean} [isAvailable] ? *\n * @property {(selection: EditorSelection, nodes: Node[]) => boolean} [isActive]\n * @property {(selection: EditorSelection, nodes: Node[]) => boolean} [isDisabled]\n *\n * @typedef {Object} ToolbarComponentItem\n * Adds a custom component to the toolbar.\n * @property {string} id\n * @property {string} groupId\n * @property {TranslatedString} title\n * @property {Function} Component\n * @property {Object} props\n * @property {(selection: EditorSelection) => boolean} [isAvailable]\n *\n * ToolbarItem.id maps to the button's `name` attribute\n * ToolbarItem.title maps to the button's `title` attribute (tooltip description)\n */\n\n/**\n * A ToolbarCommandItem must derive from a user command ( @see UserCommand )\n * specified by commandId. Properties defined in a toolbar item override those\n * from a user command.\n *\n * Example:\n *\n * resources = {\n *     user_commands: [\n *         @type {UserCommand}\n *         {\n *             id: myCommand,\n *             run: myCommandFunction,\n *             title: _t(\"My Command\"),\n *             icon: \"fa-bug\",\n *         },\n *     ],\n *     toolbar_groups: [\n *         @type {ToolbarGroup}\n *         { id: \"myGroup\" },\n *     ],\n *     toolbar_items: [\n *         @type {ToolbarCommandItem}\n *         {\n *             id: \"myButton\",\n *             groupId: \"myGroup\",\n *             commandId: \"myCommand\",\n *             title: _t(\"My Toolbar Command Button\"), // overrides the user command's `title`\n *             // `icon` is inferred from the user command\n *         },\n *         @type {ToolbarComponentItem}\n *         {\n *             id: \"myComponentButton\",\n *             groupId: \"myGroup\",\n *             title: _t(\"My Toolbar Component Button\"),\n *             Component: MyComponent,\n *             props: { myProp: \"myValue\" },\n *         },\n *     ],\n * };\n */\n\n/** Delay in ms for toolbar open after keyup, double click or triple click. */\nconst DELAY_TOOLBAR_OPEN = 300;\n\n/**\n * @typedef { Object } ToolbarShared\n * @property { ToolbarPlugin['getToolbarInfo'] } getToolbarInfo\n */\n\nexport class ToolbarPlugin extends Plugin {\n    static id = \"toolbar\";\n    static dependencies = [\"overlay\", \"selection\", \"userCommand\"];\n    static shared = [\"getToolbarInfo\"];\n    resources = {\n        selectionchange_handlers: this.handleSelectionChange.bind(this),\n        step_added_handlers: () => this.updateToolbar(),\n    };\n\n    setup() {\n        const groupIds = new Set();\n        for (const group of this.getResource(\"toolbar_groups\")) {\n            if (groupIds.has(group.id)) {\n                throw new Error(`Duplicate toolbar group id: ${group.id}`);\n            }\n            groupIds.add(group.id);\n        }\n\n        this.buttonGroups = this.getButtonGroups();\n\n        this.isMobileToolbar = hasTouch() && window.visualViewport;\n\n        if (this.isMobileToolbar) {\n            this.overlay = new MobileToolbarOverlay(this.editable);\n        } else {\n            this.overlay = this.dependencies.overlay.createOverlay(Toolbar, {\n                positionOptions: {\n                    position: \"top-start\",\n                },\n                closeOnPointerdown: false,\n            });\n        }\n        this.state = reactive({\n            buttonsActiveState: this.buttonGroups.flatMap((g) =>\n                g.buttons.map((b) => [b.id, false])\n            ),\n            buttonsDisabledState: this.buttonGroups.flatMap((g) =>\n                g.buttons.map((b) => [b.id, false])\n            ),\n            buttonsAvailableState: this.buttonGroups.flatMap((g) =>\n                g.buttons.map((b) => [b.id, true])\n            ),\n            namespace: undefined,\n        });\n        this.updateSelection = null;\n\n        this.onSelectionChangeActive = true;\n        this.debouncedUpdateToolbar = debounce(this.updateToolbar, DELAY_TOOLBAR_OPEN);\n\n        if (!this.isMobileToolbar) {\n            // Mouse interaction behavior:\n            // Close toolbar on mousedown and prevent it from opening until mouseup.\n            this.addDomListener(this.editable, \"mousedown\", () => {\n                this.overlay.close();\n                this.debouncedUpdateToolbar.cancel();\n                this.onSelectionChangeActive = false;\n            });\n            this.addDomListener(this.document, \"mouseup\", (ev) => {\n                if (ev.detail >= 2) {\n                    // Delayed open, waiting for a possible triple click.\n                    this.onSelectionChangeActive = true;\n                    this.debouncedUpdateToolbar();\n                } else {\n                    // Fast open, just wait for a possible selection change due\n                    // to mouseup.\n                    setTimeout(() => {\n                        this.updateToolbar();\n                        this.onSelectionChangeActive = true;\n                    });\n                }\n            });\n\n            // Keyboard interaction behavior:\n            // Close toolbar on keydown Arrows and prevent it from opening until\n            // keyup. Opening is debounced to avoid open/close between\n            // sequential keystrokes.\n            this.addDomListener(this.editable, \"keydown\", (ev) => {\n                if (ev.key.startsWith(\"Arrow\")) {\n                    this.overlay.close();\n                    this.onSelectionChangeActive = false;\n                }\n            });\n            this.addDomListener(this.editable, \"keyup\", (ev) => {\n                if (ev.key.startsWith(\"Arrow\")) {\n                    this.onSelectionChangeActive = true;\n                    this.debouncedUpdateToolbar();\n                }\n            });\n        }\n    }\n\n    destroy() {\n        this.debouncedUpdateToolbar.cancel();\n        this.overlay.close();\n        super.destroy();\n    }\n\n    /**\n     * @typedef {Object} ToolbarCommandButton\n     * @property {string} id\n     * @property {string} groupId\n     * @property {TranslatedString} title\n     * @property {Function} run\n     * @property {string} [icon]\n     * @property {string} [text]\n     * @property {(selection: EditorSelection) => boolean} [isAvailable]\n     * @property {(selection: EditorSelection, nodes: Node[]) => boolean} [isActive]\n     * @property {(selection: EditorSelection, nodes: Node[]) => boolean} [isDisabled]\n     *\n     * @typedef {ToolbarComponentItem} ToolbarComponentButton\n     */\n\n    /**\n     * @returns {(ToolbarCommandButton| ToolbarComponentButton)[]}\n     */\n    getButtons() {\n        /** @type {ToolbarItem[]} */\n        const toolbarItems = this.getResource(\"toolbar_items\");\n\n        /** @returns {ToolbarCommandButton} */\n        const commandItemToButton = (/** @type {ToolbarCommandItem}*/ item) => {\n            const command = this.dependencies.userCommand.getCommand(item.commandId);\n            return {\n                ...pick(command, \"title\", \"icon\", \"isAvailable\"),\n                ...omit(item, \"commandId\", \"commandParams\"),\n                run: () => command.run(item.commandParams),\n            };\n        };\n\n        return toolbarItems.map((item) => (\"Component\" in item ? item : commandItemToButton(item)));\n    }\n\n    getButtonGroups() {\n        const buttons = this.getButtons();\n        /** @type {ToolbarGroup[]} */\n        const groups = this.getResource(\"toolbar_groups\");\n\n        return groups.map((group) => ({\n            ...group,\n            buttons: buttons.filter((button) => button.groupId === group.id),\n        }));\n    }\n\n    getToolbarInfo() {\n        return {\n            buttonGroups: this.buttonGroups,\n            getSelection: () => this.dependencies.selection.getSelectionData(),\n            state: this.state,\n            focusEditable: () => this.dependencies.selection.focusEditable(),\n        };\n    }\n\n    handleSelectionChange(selectionData) {\n        if (this.onSelectionChangeActive) {\n            this.updateToolbar(selectionData);\n        }\n    }\n\n    updateToolbar(selectionData = this.dependencies.selection.getSelectionData()) {\n        this.updateToolbarVisibility(selectionData);\n        if (this.overlay.isOpen || this.config.disableFloatingToolbar) {\n            this.updateNamespace();\n            this.updateButtonsStates(selectionData.editableSelection);\n        }\n    }\n\n    getFilterTraverseNodes() {\n        return this.dependencies.selection\n            .getTraversedNodes()\n            .filter((node) => !isTextNode(node) || (node.textContent !== \"\\n\" && !isZWS(node)));\n    }\n\n    updateToolbarVisibility(selectionData) {\n        if (this.config.disableFloatingToolbar) {\n            return;\n        }\n\n        if (this.shouldBeVisible(selectionData)) {\n            // Open toolbar or update its position\n            const props = { toolbar: this.getToolbarInfo(), class: \"shadow rounded my-2\" };\n            this.overlay.open({ props });\n        } else if (this.overlay.isOpen && !this.shouldPreventClosing(selectionData)) {\n            // Close toolbar\n            this.overlay.close();\n        }\n    }\n\n    shouldBeVisible(selectionData) {\n        const inEditable =\n            selectionData.documentSelectionIsInEditable &&\n            !selectionData.documentSelectionIsProtected &&\n            !selectionData.documentSelectionIsProtecting;\n        if (!inEditable) {\n            return false;\n        }\n        if (this.isMobileToolbar) {\n            return true;\n        }\n        const isCollapsed = selectionData.editableSelection.isCollapsed;\n        if (isCollapsed) {\n            return !!closestElement(selectionData.editableSelection.anchorNode, \"td.o_selected_td\");\n        }\n        return this.getFilterTraverseNodes().length;\n    }\n\n    shouldPreventClosing(selectionData) {\n        const preventClosing = selectionData.documentSelection?.anchorNode?.closest?.(\n            \"[data-prevent-closing-overlay]\"\n        );\n        return preventClosing?.dataset?.preventClosingOverlay === \"true\";\n    }\n\n    updateNamespace() {\n        const traversedNodes = this.getFilterTraverseNodes();\n        for (const namespace of this.getResource(\"toolbar_namespaces\")) {\n            if (namespace.isApplied(traversedNodes)) {\n                this.state.namespace = namespace.id;\n                return;\n            }\n        }\n        this.state.namespace = undefined;\n    }\n\n    updateButtonsStates(selection) {\n        if (!this.updateSelection) {\n            queueMicrotask(() => {\n                if (!this.isDestroyed) {\n                    this._updateButtonsStates();\n                }\n            });\n        }\n        this.updateSelection = selection;\n    }\n    _updateButtonsStates() {\n        const selection = this.updateSelection;\n        if (!selection) {\n            return;\n        }\n        const nodes = this.getFilterTraverseNodes();\n        for (const buttonGroup of this.buttonGroups) {\n            if (buttonGroup.namespace === this.state.namespace) {\n                for (const button of buttonGroup.buttons) {\n                    this.state.buttonsActiveState[button.id] = button.isActive?.(selection, nodes);\n                    this.state.buttonsDisabledState[button.id] = button.isDisabled?.(\n                        selection,\n                        nodes\n                    );\n                    this.state.buttonsAvailableState[button.id] =\n                        button.isAvailable === undefined || button.isAvailable(selection);\n                }\n            }\n        }\n        this.updateSelection = null;\n    }\n}\n\nclass MobileToolbarOverlay {\n    constructor(editable) {\n        this.isOpen = false;\n        this.overlayId = `mobile_toolbar_${Math.random().toString(16).slice(2)}`;\n        this.editable = editable;\n    }\n\n    open({ props }) {\n        props.class = \"shadow\";\n        if (!this.isOpen) {\n            const modal = this.editable.closest(\".o_modal_full\");\n            if (modal) {\n                // Same height of the toolbar\n                modal.style.paddingBottom = \"40px\";\n            }\n            registry.category(\"main_components\").add(this.overlayId, {\n                Component: ToolbarMobile,\n                props,\n            });\n            this.isOpen = true;\n        }\n    }\n\n    close() {\n        const modal = this.editable.closest(\".o_modal_full\");\n        if (modal) {\n            modal.style.paddingBottom = \"\";\n        }\n        registry.category(\"main_components\").remove(this.overlayId, \"MobileToolbar\");\n        this.isOpen = false;\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { Plugin } from \"../plugin\";\nimport { VideoSelector } from \"./media/media_dialog/video_selector\";\n\nexport const YOUTUBE_URL_GET_VIDEO_ID =\n    /^(?:(?:https?:)?\\/\\/)?(?:(?:www|m)\\.)?(?:youtube\\.com|youtu\\.be)(?:\\/(?:[\\w-]+\\?v=|embed\\/|v\\/)?)([^\\s?&#]+)(?:\\S+)?$/i;\n\nexport class YoutubePlugin extends Plugin {\n    static id = \"youtube\";\n    static dependencies = [\"history\", \"powerbox\", \"link\", \"dom\"];\n    resources = {\n        paste_url_overrides: this.handlePasteUrl.bind(this),\n    };\n    /**\n     * @param {string} text\n     * @param {string} url\n     */\n    handlePasteUrl(text, url) {\n        // to know if this logic should be executed or not. Do we still want an\n        // option of do we want to add a plugin whenever we want the feature?\n        const youtubeUrl = !this.config.disableVideo && YOUTUBE_URL_GET_VIDEO_ID.exec(url);\n        if (youtubeUrl) {\n            const restoreSavepoint = this.dependencies.history.makeSavePoint();\n            // Open powerbox with commands to embed media or paste as link.\n            // Insert URL as text, revert it later if a command is triggered.\n            this.dependencies.dom.insert(text);\n            this.dependencies.history.addStep();\n            // URL is a YouTube video.\n            const embedVideoCommand = {\n                title: _t(\"Embed Youtube Video\"),\n                description: _t(\"Embed the youtube video in the document.\"),\n                icon: \"fa-youtube-play\",\n                run: async () => {\n                    const videoElement = await this.getYoutubeVideoElement(youtubeUrl[0]);\n                    this.dependencies.dom.insert(videoElement);\n                    this.dependencies.history.addStep();\n                },\n            };\n            const commands = [\n                embedVideoCommand,\n                this.dependencies.link.getPathAsUrlCommand(text, url),\n            ];\n            this.dependencies.powerbox.openPowerbox({ commands, onApplyCommand: restoreSavepoint });\n            return true;\n        }\n    }\n    // @todo @phoenix: Should this be in this plugin?\n    /**\n     * @param {string} url\n     */\n    async getYoutubeVideoElement(url) {\n        const { embed_url: src } = await rpc(\"/html_editor/video_url/data\", {\n            video_url: url,\n        });\n        const [savedVideo] = VideoSelector.createElements([{ src }]);\n        savedVideo.classList.add(...VideoSelector.mediaSpecificClasses);\n        return savedVideo;\n    }\n}\n", "const urlParams = new URLSearchParams(window.location.search);\nconst collaborationDebug = urlParams.get(\"collaborationDebug\");\nconst COLLABORATION_LOCALSTORAGE_KEY = \"odoo_editor_collaboration_debug\";\nif (typeof collaborationDebug === \"string\") {\n    if (collaborationDebug === \"false\") {\n        localStorage.removeItem(\n            COLLABORATION_LOCALSTORAGE_KEY,\n            urlParams.get(\"collaborationDebug\")\n        );\n    } else {\n        localStorage.setItem(COLLABORATION_LOCALSTORAGE_KEY, urlParams.get(\"collaborationDebug\"));\n    }\n}\nconst debugValue = localStorage.getItem(COLLABORATION_LOCALSTORAGE_KEY);\n\nconst debugShowLog = [\"\", \"true\", \"all\"].includes(debugValue);\nconst debugShowNotifications = debugValue === \"all\";\n\nconst baseNotificationMethods = {\n    ptp_request: async function (notification) {\n        const { requestId, requestName, requestPayload, requestTransport } =\n            notification.notificationPayload;\n        this._onRequest(\n            notification.fromPeerId,\n            requestId,\n            requestName,\n            requestPayload,\n            requestTransport\n        );\n    },\n    ptp_request_result: function (notification) {\n        const { requestId, result } = notification.notificationPayload;\n        // If not in _pendingRequestResolver, it means it has timeout.\n        if (this._pendingRequestResolver[requestId]) {\n            clearTimeout(this._pendingRequestResolver[requestId].rejectTimeout);\n            this._pendingRequestResolver[requestId].resolve(result);\n            delete this._pendingRequestResolver[requestId];\n        }\n    },\n\n    ptp_join: async function (notification) {\n        const peerId = notification.fromPeerId;\n        if (this.peersInfos[peerId] && this.peersInfos[peerId].peerConnection) {\n            return this.peersInfos[peerId];\n        }\n        this._createPeer(peerId);\n    },\n\n    rtc_signal_icecandidate: async function (notification) {\n        if (debugShowLog) {\n            console.log(`%creceive candidate`, \"background: darkgreen; color: white;\");\n        }\n        const peerInfos = this.peersInfos[notification.fromPeerId];\n        if (\n            !peerInfos ||\n            !peerInfos.peerConnection ||\n            peerInfos.peerConnection.connectionState === \"closed\"\n        ) {\n            console.groupCollapsed(\"=== ERROR: Handle Ice Candidate from undefined|closed ===\");\n            console.trace(peerInfos);\n            console.groupEnd();\n            return;\n        }\n        if (!peerInfos.peerConnection.remoteDescription) {\n            peerInfos.iceCandidateBuffer.push(notification.notificationPayload);\n        } else {\n            this._addIceCandidate(peerInfos, notification.notificationPayload);\n        }\n    },\n    rtc_signal_description: async function (notification) {\n        const description = notification.notificationPayload;\n        if (debugShowLog) {\n            console.log(\n                `%cdescription received:`,\n                \"background: blueviolet; color: white;\",\n                description\n            );\n        }\n\n        const peerInfos =\n            this.peersInfos[notification.fromPeerId] || this._createPeer(notification.fromPeerId);\n        const pc = peerInfos.peerConnection;\n\n        if (!pc || pc.connectionState === \"closed\") {\n            if (debugShowLog) {\n                console.groupCollapsed(\"=== ERROR: handle offer ===\");\n                console.log(\n                    \"An offer has been received for a non-existent peer connection - peer: \" +\n                        notification.fromPeerId\n                );\n                console.trace(pc && pc.connectionState);\n                console.groupEnd();\n            }\n            return;\n        }\n\n        // Skip if we already have an offer.\n        if (pc.signalingState === \"have-remote-offer\") {\n            return;\n        }\n\n        // If there is a racing conditing with the signaling offer (two\n        // being sent at the same time). We need one peer that abort by\n        // rollbacking to a stable signaling state where the other is\n        // continuing the process. The peer that is polite is the one that\n        // will rollback.\n        const isPolite =\n            (\"\" + notification.fromPeerId).localeCompare(\"\" + this._currentPeerId) === 1;\n        if (debugShowLog) {\n            console.log(\n                `%cisPolite: %c${isPolite}`,\n                \"background: deepskyblue;\",\n                `background:${isPolite ? \"green\" : \"red\"}`\n            );\n        }\n\n        const isOfferRacing =\n            description.type === \"offer\" &&\n            (peerInfos.makingOffer || pc.signalingState !== \"stable\");\n        // If there is a racing conditing with the signaling offer and the\n        // peer is impolite, we must not process this offer and wait for\n        // the answer for the signaling process to continue.\n        if (isOfferRacing && !isPolite) {\n            if (debugShowLog) {\n                console.log(\n                    `%creturn because isOfferRacing && !isPolite. pc.signalingState: ${pc.signalingState}`,\n                    \"background: red;\"\n                );\n            }\n            return;\n        }\n        if (debugShowLog) {\n            console.log(`%cisOfferRacing: ${isOfferRacing}`, \"background: red;\");\n            console.log(`%c SETREMOTEDESCRIPTION`, \"background: navy; color:white;\");\n        }\n        try {\n            await pc.setRemoteDescription(description);\n        } catch (e) {\n            if (e instanceof DOMException && e.name === \"InvalidStateError\") {\n                console.error(e);\n                return;\n            } else {\n                throw e;\n            }\n        }\n        if (peerInfos.iceCandidateBuffer.length) {\n            for (const candidate of peerInfos.iceCandidateBuffer) {\n                await this._addIceCandidate(peerInfos, candidate);\n            }\n            peerInfos.iceCandidateBuffer.splice(0);\n        }\n        if (description.type === \"offer\") {\n            const answerDescription = await pc.createAnswer();\n            try {\n                await pc.setLocalDescription(answerDescription);\n            } catch (e) {\n                if (e instanceof DOMException && e.name === \"InvalidStateError\") {\n                    console.error(e);\n                    return;\n                } else {\n                    throw e;\n                }\n            }\n            this.notifyPeer(notification.fromPeerId, \"rtc_signal_description\", pc.localDescription);\n        }\n    },\n};\n\nexport class PeerToPeer {\n    constructor(options) {\n        this.options = options;\n        this._currentPeerId = this.options.currentPeerId;\n        if (debugShowLog) {\n            console.log(\n                `%c currentPeerId:${this._currentPeerId}`,\n                \"background: blue; color: white;\"\n            );\n        }\n\n        // peerId -> PeerInfos\n        this.peersInfos = {};\n        this._lastRequestId = -1;\n        this._pendingRequestResolver = {};\n        this._stopped = false;\n    }\n\n    stop() {\n        this.closeAllConnections();\n        this._stopped = true;\n    }\n\n    getConnectedPeerIds() {\n        return Object.entries(this.peersInfos)\n            .filter(\n                ([id, infos]) =>\n                    infos.peerConnection &&\n                    infos.peerConnection.iceConnectionState === \"connected\" &&\n                    infos.dataChannel &&\n                    infos.dataChannel.readyState === \"open\"\n            )\n            .map(([id]) => id);\n    }\n\n    removePeer(peerId) {\n        if (debugShowLog) {\n            console.log(`%c REMOVE PEER ${peerId}`, \"background: chocolate;\");\n        }\n        this.notifySelf(\"ptp_remove\", peerId);\n        const peerInfos = this.peersInfos[peerId];\n        if (!peerInfos) {\n            return;\n        }\n        clearTimeout(peerInfos.fallbackTimeout);\n        clearTimeout(peerInfos.zombieTimeout);\n        peerInfos.dataChannel && peerInfos.dataChannel.close();\n        peerInfos.peerConnection && peerInfos.peerConnection.close();\n        delete this.peersInfos[peerId];\n    }\n\n    closeAllConnections() {\n        for (const peerId of Object.keys(this.peersInfos)) {\n            this.notifyAllPeers(\"ptp_disconnect\");\n            this.removePeer(peerId);\n        }\n    }\n\n    async notifyAllPeers(notificationName, notificationPayload, { transport = \"server\" } = {}) {\n        if (this._stopped) {\n            return;\n        }\n        const transportPayload = {\n            fromPeerId: this._currentPeerId,\n            notificationName,\n            notificationPayload,\n        };\n        if (transport === \"server\") {\n            await this.options.broadcastAll(transportPayload);\n        } else if (transport === \"rtc\") {\n            for (const cliendId of Object.keys(this.peersInfos)) {\n                this._channelNotify(cliendId, transportPayload);\n            }\n        } else {\n            throw new Error(\n                `Transport \"${transport}\" is not supported. Use \"server\" or \"rtc\" transport.`\n            );\n        }\n    }\n\n    notifyPeer(peerId, notificationName, notificationPayload, { transport = \"server\" } = {}) {\n        if (this._stopped) {\n            return;\n        }\n        if (debugShowNotifications) {\n            if (notificationName === \"ptp_request_result\") {\n                console.log(\n                    `%c${Date.now()} - REQUEST RESULT SEND: %c${transport}:${\n                        notificationPayload.requestId\n                    }:${this._currentPeerId.slice(\"-5\")}:${peerId.slice(\"-5\")}`,\n                    \"color: #aaa;font-weight:bold;\",\n                    \"color: #aaa;font-weight:normal\"\n                );\n            } else if (notificationName === \"ptp_request\") {\n                console.log(\n                    `%c${Date.now()} - REQUEST SEND: %c${transport}:${\n                        notificationPayload.requestName\n                    }|${notificationPayload.requestId}:${this._currentPeerId.slice(\n                        \"-5\"\n                    )}:${peerId.slice(\"-5\")}`,\n                    \"color: #aaa;font-weight:bold;\",\n                    \"color: #aaa;font-weight:normal\"\n                );\n            } else {\n                console.log(\n                    `%c${Date.now()} - NOTIFICATION SEND: %c${transport}:${notificationName}:${this._currentPeerId.slice(\n                        \"-5\"\n                    )}:${peerId.slice(\"-5\")}`,\n                    \"color: #aaa;font-weight:bold;\",\n                    \"color: #aaa;font-weight:normal\"\n                );\n            }\n        }\n        const transportPayload = {\n            fromPeerId: this._currentPeerId,\n            toPeerId: peerId,\n            notificationName,\n            notificationPayload,\n        };\n        if (transport === \"server\") {\n            this.options.broadcastAll(transportPayload);\n        } else if (transport === \"rtc\") {\n            this._channelNotify(peerId, transportPayload);\n        } else {\n            throw new Error(\n                `Transport \"${transport}\" is not supported. Use \"server\" or \"rtc\" transport.`\n            );\n        }\n    }\n\n    notifySelf(notificationName, notificationPayload) {\n        if (this._stopped) {\n            return;\n        }\n        return this.handleNotification({ notificationName, notificationPayload });\n    }\n\n    handleNotification(notification) {\n        if (this._stopped) {\n            return;\n        }\n        const isInternalNotification =\n            typeof notification.fromPeerId === \"undefined\" &&\n            typeof notification.toPeerId === \"undefined\";\n        if (\n            isInternalNotification ||\n            (notification.fromPeerId !== this._currentPeerId && !notification.toPeerId) ||\n            notification.toPeerId === this._currentPeerId\n        ) {\n            if (debugShowNotifications) {\n                if (notification.notificationName === \"ptp_request_result\") {\n                    console.log(\n                        `%c${Date.now()} - REQUEST RESULT RECEIVE: %c${\n                            notification.notificationPayload.requestId\n                        }:${notification.fromPeerId.slice(\"-5\")}:${notification.toPeerId.slice(\n                            \"-5\"\n                        )}`,\n                        \"color: #aaa;font-weight:bold;\",\n                        \"color: #aaa;font-weight:normal\"\n                    );\n                } else if (notification.notificationName === \"ptp_request\") {\n                    console.log(\n                        `%c${Date.now()} - REQUEST RECEIVE: %c${\n                            notification.notificationPayload.requestName\n                        }|${\n                            notification.notificationPayload.requestId\n                        }:${notification.fromPeerId.slice(\"-5\")}:${notification.toPeerId.slice(\n                            \"-5\"\n                        )}`,\n                        \"color: #aaa;font-weight:bold;\",\n                        \"color: #aaa;font-weight:normal\"\n                    );\n                } else {\n                    console.log(\n                        `%c${Date.now()} - NOTIFICATION RECEIVE: %c${\n                            notification.notificationName\n                        }:${notification.fromPeerId}:${notification.toPeerId}`,\n                        \"color: #aaa;font-weight:bold;\",\n                        \"color: #aaa;font-weight:normal\"\n                    );\n                }\n            }\n            try {\n                const baseMethod = baseNotificationMethods[notification.notificationName];\n                if (baseMethod) {\n                    return baseMethod.call(this, notification);\n                }\n                if (this.options.onNotification) {\n                    return this.options.onNotification(notification);\n                }\n            } catch (error) {\n                console.groupCollapsed(\"=== ERROR: On notification in collaboration ===\");\n                console.error(error);\n                console.groupEnd();\n            }\n        }\n    }\n\n    requestPeer(peerId, requestName, requestPayload, { transport = \"server\" } = {}) {\n        if (this._stopped) {\n            return;\n        }\n        return new Promise((resolve, reject) => {\n            const requestId = this._getRequestId();\n\n            const abort = (reason) => {\n                clearTimeout(rejectTimeout);\n                delete this._pendingRequestResolver[requestId];\n                reject(new RequestError(reason || \"Request was aborted.\"));\n            };\n            const rejectTimeout = setTimeout(\n                () => abort(\"Request took too long (more than 10 seconds).\"),\n                10000\n            );\n\n            this._pendingRequestResolver[requestId] = {\n                resolve,\n                rejectTimeout,\n                abort,\n            };\n\n            this.notifyPeer(\n                peerId,\n                \"ptp_request\",\n                {\n                    requestId,\n                    requestName,\n                    requestPayload,\n                    requestTransport: transport,\n                },\n                { transport }\n            );\n        });\n    }\n    abortCurrentRequests() {\n        for (const { abort } of Object.values(this._pendingRequestResolver)) {\n            abort();\n        }\n    }\n    _createPeer(peerId, { makeOffer = true } = {}) {\n        if (this._stopped) {\n            return;\n        }\n        if (debugShowLog) {\n            console.log(\"CREATE CONNECTION with peer id:\", peerId);\n        }\n        this.peersInfos[peerId] = {\n            makingOffer: false,\n            iceCandidateBuffer: [],\n            backoffFactor: 0,\n        };\n\n        if (!navigator.onLine) {\n            return this.peersInfos[peerId];\n        }\n        const pc = new RTCPeerConnection(this.options.peerConnectionConfig);\n\n        if (makeOffer) {\n            pc.onnegotiationneeded = async () => {\n                if (debugShowLog) {\n                    console.log(\n                        `%c NEGONATION NEEDED: ${pc.connectionState}`,\n                        \"background: deeppink;\"\n                    );\n                }\n                try {\n                    this.peersInfos[peerId].makingOffer = true;\n                    if (debugShowLog) {\n                        console.log(\n                            `%ccreating and sending an offer`,\n                            \"background: darkmagenta; color: white;\"\n                        );\n                    }\n                    const offer = await pc.createOffer();\n                    // Avoid race condition.\n                    if (pc.signalingState !== \"stable\") {\n                        return;\n                    }\n                    await pc.setLocalDescription(offer);\n                    this.notifyPeer(peerId, \"rtc_signal_description\", pc.localDescription);\n                } catch (err) {\n                    console.error(err);\n                } finally {\n                    this.peersInfos[peerId].makingOffer = false;\n                }\n            };\n        }\n        pc.onicecandidate = async (event) => {\n            if (event.candidate) {\n                this.notifyPeer(peerId, \"rtc_signal_icecandidate\", event.candidate);\n            }\n        };\n        pc.oniceconnectionstatechange = async () => {\n            if (debugShowLog) {\n                console.log(\"ICE STATE UPDATE: \" + pc.iceConnectionState);\n            }\n\n            switch (pc.iceConnectionState) {\n                case \"failed\":\n                case \"closed\":\n                    this.removePeer(peerId);\n                    break;\n                case \"disconnected\":\n                    if (navigator.onLine) {\n                        await this._recoverConnection(peerId, {\n                            delay: 3000,\n                            reason: \"ice connection disconnected\",\n                        });\n                    }\n                    break;\n                case \"connected\":\n                    this.peersInfos[peerId].backoffFactor = 0;\n                    break;\n            }\n        };\n        // This event does not work in FF. Let's try with oniceconnectionstatechange if it is sufficient.\n        pc.onconnectionstatechange = async () => {\n            if (debugShowLog) {\n                console.log(\"CONNECTION STATE UPDATE:\" + pc.connectionState);\n            }\n\n            switch (pc.connectionState) {\n                case \"failed\":\n                case \"closed\":\n                    this.removePeer(peerId);\n                    break;\n                case \"disconnected\":\n                    if (navigator.onLine) {\n                        await this._recoverConnection(peerId, {\n                            delay: 3000,\n                            reason: \"connection disconnected\",\n                        });\n                    }\n                    break;\n                case \"connected\":\n                case \"completed\":\n                    this.peersInfos[peerId].backoffFactor = 0;\n                    break;\n            }\n        };\n        pc.onicecandidateerror = async (error) => {\n            if (debugShowLog) {\n                console.groupCollapsed(\"=== ERROR: onIceCandidate ===\");\n                console.log(\n                    \"connectionState: \" +\n                        pc.connectionState +\n                        \" - iceState: \" +\n                        pc.iceConnectionState\n                );\n                console.trace(error);\n                console.groupEnd();\n            }\n            this._recoverConnection(peerId, { delay: 3000, reason: \"ice candidate error\" });\n        };\n        const dataChannel = pc.createDataChannel(\"notifications\", { negotiated: true, id: 1 });\n        let message = [];\n        dataChannel.onmessage = (event) => {\n            if (event.data !== \"-\") {\n                message.push(event.data);\n            } else {\n                this.handleNotification(JSON.parse(message.join(\"\")));\n                message = [];\n            }\n        };\n        dataChannel.onopen = (event) => {\n            this.notifySelf(\"rtc_data_channel_open\", {\n                connectionPeerId: peerId,\n            });\n        };\n\n        this.peersInfos[peerId].peerConnection = pc;\n        this.peersInfos[peerId].dataChannel = dataChannel;\n\n        return this.peersInfos[peerId];\n    }\n    async _addIceCandidate(peerInfos, candidate) {\n        const rtcIceCandidate = new RTCIceCandidate(candidate);\n        try {\n            await peerInfos.peerConnection.addIceCandidate(rtcIceCandidate);\n        } catch (error) {\n            // Ignored.\n            console.groupCollapsed(\"=== ERROR: ADD ICE CANDIDATE ===\");\n            console.trace(error);\n            console.groupEnd();\n        }\n    }\n\n    _channelNotify(peerId, transportPayload) {\n        if (this._stopped) {\n            return;\n        }\n        const peerInfo = this.peersInfos[peerId];\n        const dataChannel = peerInfo && peerInfo.dataChannel;\n\n        if (!dataChannel || dataChannel.readyState !== \"open\") {\n            if (peerInfo && !peerInfo.zombieTimeout) {\n                if (debugShowLog) {\n                    console.warn(\n                        `Impossible to communicate with peer ${peerId}. The connection will be killed in 10 seconds if the datachannel state has not changed.`\n                    );\n                }\n                this._killPotentialZombie(peerId);\n            }\n        } else {\n            const str = JSON.stringify(transportPayload);\n            const size = str.length;\n            const maxStringLength = 5000;\n            let from = 0;\n            let to = maxStringLength;\n            while (from < size) {\n                dataChannel.send(str.slice(from, to));\n                from = to;\n                to = to += maxStringLength;\n            }\n            dataChannel.send(\"-\");\n        }\n    }\n\n    _getRequestId() {\n        this._lastRequestId++;\n        return this._lastRequestId;\n    }\n\n    async _onRequest(fromPeerId, requestId, requestName, requestPayload, requestTransport) {\n        if (this._stopped) {\n            return;\n        }\n        const requestFunction = this.options.onRequest && this.options.onRequest[requestName];\n        const result = await requestFunction({\n            fromPeerId,\n            requestId,\n            requestName,\n            requestPayload,\n        });\n        this.notifyPeer(\n            fromPeerId,\n            \"ptp_request_result\",\n            { requestId, result },\n            { transport: requestTransport }\n        );\n    }\n    /**\n     * Attempts a connection recovery by updating the tracks, which will start\n     * a new transaction: negotiationneeded -> offer -> answer -> ...\n     *\n     * @private\n     * @param {Object} [param1]\n     * @param {number} [param1.delay] in ms\n     * @param {string} [param1.reason]\n     */\n    _recoverConnection(peerId, { delay = 0, reason = \"\" } = {}) {\n        if (this._stopped) {\n            this.removePeer(peerId);\n            return;\n        }\n        const peerInfos = this.peersInfos[peerId];\n        if (!peerInfos || peerInfos.fallbackTimeout) {\n            return;\n        }\n        const backoffFactor = this.peersInfos[peerId].backoffFactor;\n        const backoffDelay = delay * Math.pow(2, backoffFactor);\n        // Stop trying to recover the connection after 10 attempts.\n        if (backoffFactor > 10) {\n            if (debugShowLog) {\n                console.log(\n                    `%c STOP RTC RECOVERY: impossible to connect to peer ${peerId}: ${reason}`,\n                    \"background: darkred; color: white;\"\n                );\n            }\n            return;\n        }\n\n        peerInfos.fallbackTimeout = setTimeout(async () => {\n            peerInfos.fallbackTimeout = undefined;\n            const pc = peerInfos.peerConnection;\n            if (!pc || pc.iceConnectionState === \"connected\") {\n                return;\n            }\n            if ([\"connected\", \"closed\"].includes(pc.connectionState)) {\n                return;\n            }\n            // hard reset: recreating a RTCPeerConnection\n            if (debugShowLog) {\n                console.log(\n                    `%c RTC RECOVERY: calling back peer ${peerId} to salvage the connection ${pc.iceConnectionState} after ${backoffDelay}ms, reason: ${reason}`,\n                    \"background: darkorange; color: white;\"\n                );\n            }\n            this.removePeer(peerId);\n            const newPeerInfos = this._createPeer(peerId);\n            newPeerInfos.backoffFactor = backoffFactor + 1;\n        }, backoffDelay);\n    }\n    // todo: do we try to salvage the connection after killing the zombie ?\n    // Maybe the salvage should be done when the connection is dropped.\n    _killPotentialZombie(peerId) {\n        if (this._stopped) {\n            this.removePeer(peerId);\n            return;\n        }\n        const peerInfos = this.peersInfos[peerId];\n        if (!peerInfos || peerInfos.zombieTimeout) {\n            return;\n        }\n\n        // If there is no connection after 10 seconds, terminate.\n        peerInfos.zombieTimeout = setTimeout(() => {\n            if (peerInfos && peerInfos.dataChannel && peerInfos.dataChannel.readyState !== \"open\") {\n                if (debugShowLog) {\n                    console.log(`%c KILL ZOMBIE ${peerId}`, \"background: red;\");\n                }\n                this.removePeer(peerId);\n            } else {\n                if (debugShowLog) {\n                    console.log(`%c NOT A ZOMBIE ${peerId}`, \"background: green;\");\n                }\n            }\n        }, 10000);\n    }\n}\n\nexport class RequestError extends Error {\n    constructor(message) {\n        super(message);\n        this.name = \"RequestError\";\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { user } from \"@web/core/user\";\nimport { Mutex } from \"@web/core/utils/concurrency\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { PeerToPeer, RequestError } from \"./PeerToPeer\";\nimport { ancestors } from \"@html_editor/utils/dom_traversal\";\nimport { childNodeIndex } from \"@html_editor/utils/position\";\n\n/**\n * @typedef {Object} CollaborationSelection\n * @property {import(\"@html_editor/core/history_plugin\").SerializedSelection} selection\n * @property {string} color\n * @property {string} peerId\n */\n\n// Time to consider a user offline in ms. This fixes the problem of the\n// navigator closing rtc connection when the mac laptop screen is closed.\n// const CONSIDER_OFFLINE_TIME = 1000;\n// Check wether the computer could be offline. This fixes the problem of the\n// navigator closing rtc connection when the mac laptop screen is closed.\n// This case happens on Mac OS on every browser when the user close it's laptop\n// screen. At first, the os/navigator closes all rtc connection, and after some\n// times, the os/navigator internet goes offline without triggering an\n// offline/online event.\n// However, if the laptop screen is open and the connection is properly remove\n// (e.g. disconnect wifi), the event is properly triggered.\n// const CHECK_OFFLINE_TIME = 1000;\n// const PTP_PEER_DISCONNECTED_STATES = [\"failed\", \"closed\", \"disconnected\"];\n\n// Time in ms to wait when trying to aggregate snapshots from other peers and\n// potentially recover from a missing step before trying to apply those\n// snapshots or recover from the server.\nconst PTP_MAX_RECOVERY_TIME = 500;\n\nconst REQUEST_ERROR = Symbol(\"REQUEST_ERROR\");\n\n// this is a local cache for ice server descriptions\nlet ICE_SERVERS = null;\n\n/**\n * @typedef { Object } CollaborationOdooShared\n * @property { CollaborationOdooPlugin['getPeerMetadata'] } getPeerMetadata\n */\n\nexport class CollaborationOdooPlugin extends Plugin {\n    static id = \"collaborationOdoo\";\n    static dependencies = [\"baseContainer\", \"history\", \"collaboration\", \"selection\"];\n    static shared = [\"getPeerMetadata\"];\n    resources = {\n        selectionchange_handlers: debounce(() => {\n            this.ptp?.notifyAllPeers(\n                \"oe_history_set_selection\",\n                this.getCurrentCollaborativeSelection(),\n                {\n                    transport: \"rtc\",\n                }\n            );\n        }, 50),\n        clean_for_save_handlers: ({ root }) => this.attachHistoryIds(root),\n        history_missing_parent_step_handlers: this.onHistoryMissingParentStep.bind(this),\n        history_reset_handlers: this.onReset.bind(this),\n        step_added_handlers: ({ step }) =>\n            this.ptp?.notifyAllPeers(\"oe_history_step\", step, { transport: \"rtc\" }),\n    };\n\n    setup() {\n        this.isDocumentStale = false;\n\n        this.ptpJoined = false;\n\n        // Each time a reset of the document is triggered, it is assigned a\n        // unique identifier. Since resetting the editor involves asynchronous\n        // requests, it is possible that subsequent resets are triggered before\n        // the previous one is complete. This property identifies the latest\n        // reset and can be compared against to cancel the processing of late\n        // responses from previous resets.\n        this.lastCollaborationResetId = 0;\n\n        // The ID is the latest step ID that the server knows through\n        // `data-last-history-steps`. We cannot save to the server if we do not\n        // have that ID in our history ids as it means that our version is\n        // stale.\n        this.serverLastStepId =\n            this.config.content && this.getLastHistoryStepId(this.config.content);\n\n        this.setupCollaboration(this.config.collaboration.collaborationChannel);\n\n        const collaborativeTrigger = this.config.collaboration.collaborativeTrigger;\n        this.joinPeerToPeer = this.joinPeerToPeer.bind(this);\n        if (collaborativeTrigger === \"start\") {\n            this.joinPeerToPeer();\n        } else if (\n            collaborativeTrigger === \"focus\" ||\n            typeof collaborativeTrigger === \"undefined\"\n        ) {\n            // Wait until editor is focused to join the peer to peer network.\n            this.editable.addEventListener(\"focus\", this.joinPeerToPeer);\n        }\n\n        stripHistoryIds(this.editable);\n    }\n    destroy() {\n        this.collaborationStopBus && this.collaborationStopBus();\n        // If peer to peer is initializing, wait for properly closing it.\n        if (this.peerToPeerLoading) {\n            this.peerToPeerLoading.then(() => {\n                this.stopPeerToPeer();\n            });\n        }\n        // todo: to implement\n        // clearInterval(this.collaborationInterval);\n        super.destroy();\n    }\n\n    stopPeerToPeer() {\n        this.joiningPtp = false;\n        this.ptpJoined = false;\n        this.resetCollabRequests();\n        this.ptp && this.ptp.stop();\n    }\n\n    getCurrentCollaborativeSelection() {\n        const selection = this.dependencies.selection.getEditableSelection();\n        return {\n            selection: this.dependencies.history.serializeSelection(selection),\n            peerId: this.config.collaboration.peerId,\n        };\n    }\n    setupCollaboration(collaborationChannel) {\n        const modelName = collaborationChannel.collaborationModelName;\n        const fieldName = collaborationChannel.collaborationFieldName;\n        const resId = collaborationChannel.collaborationResId;\n        const channelName = `editor_collaboration:${modelName}:${fieldName}:${resId}`;\n\n        if (\n            !(modelName && fieldName && resId)\n            // todo: handle this feature\n            // || Wysiwyg.activeCollaborationChannelNames.has(channelName)\n        ) {\n            return;\n        }\n\n        this.collaborationChannelName = channelName;\n        this.historyStepsBuffer = [];\n        // Wysiwyg.activeCollaborationChannelNames.add(channelName);\n\n        const collaborationBusListener = (payload) => {\n            if (\n                payload.model_name === modelName &&\n                payload.field_name === fieldName &&\n                payload.res_id === resId\n            ) {\n                if (payload.notificationName === \"html_field_write\") {\n                    this.onServerLastIdUpdate(payload.notificationPayload.last_step_id);\n                } else if (this.ptpJoined) {\n                    this.peerToPeerLoading.then(() => this.ptp.handleNotification(payload));\n                }\n            }\n        };\n        const { busService } = this.config.collaboration;\n        busService.subscribe(\"editor_collaboration\", collaborationBusListener);\n        busService.addChannel(this.collaborationChannelName);\n        this.collaborationStopBus = () => {\n            // Wysiwyg.activeCollaborationChannelNames.delete(this.collaborationChannelName);\n            busService.unsubscribe(\"editor_collaboration\", collaborationBusListener);\n            busService.deleteChannel(this.collaborationChannelName);\n        };\n\n        this.startCollaborationTime = new Date().getTime();\n\n        // this.checkConnectionChange = () => {\n        //     if (!this.ptp) {\n        //         return;\n        //     }\n        //     if (!navigator.onLine) {\n        //         this.signalOffline();\n        //     } else {\n        //         this.signalOnline();\n        //     }\n        // };\n\n        // window.addEventListener(\"online\", this.checkConnectionChange);\n        // window.addEventListener(\"offline\", this.checkConnectionChange);\n\n        // this.collaborationInterval = setInterval(async () => {\n        //     if (this.offlineTimeout || this.preSavePromise || !this.ptp) {\n        //         return;\n        //     }\n\n        //     const peersInfos = Object.values(this.ptp.peersInfos);\n        //     const couldBeDisconnected =\n        //         Boolean(peersInfos.length) &&\n        //         peersInfos.every((x) =>\n        //             PTP_PEER_DISCONNECTED_STATES.includes(\n        //                 x.peerConnection && x.peerConnection.connectionState\n        //             )\n        //         );\n\n        //     if (couldBeDisconnected) {\n        //         this.offlineTimeout = setTimeout(() => {\n        //             this.signalOffline();\n        //         }, CONSIDER_OFFLINE_TIME);\n        //     }\n        // }, CHECK_OFFLINE_TIME);\n\n        const loadPeerToPeer = async () => {\n            if (!ICE_SERVERS) {\n                ICE_SERVERS = await rpc(\"/html_editor/get_ice_servers\");\n            }\n\n            let iceServers = ICE_SERVERS;\n            if (!iceServers.length) {\n                iceServers = [\n                    {\n                        urls: [\"stun:stun1.l.google.com:19302\", \"stun:stun2.l.google.com:19302\"],\n                    },\n                ];\n            }\n            this.iceServers = iceServers;\n\n            this.ptp = this.getNewPtp();\n        };\n\n        this.peerToPeerLoading = loadPeerToPeer();\n    }\n\n    getNewPtp() {\n        const rpcMutex = new Mutex();\n        const { collaborationChannel } = this.config.collaboration;\n        const modelName = collaborationChannel.collaborationModelName;\n        const fieldName = collaborationChannel.collaborationFieldName;\n        const resId = collaborationChannel.collaborationResId;\n\n        // Wether or not the history has been sent or received at least\n        // once.\n        this.historySyncAtLeastOnce = false;\n\n        return new PeerToPeer({\n            peerConnectionConfig: { iceServers: this.iceServers },\n            currentPeerId: this.config.collaboration.peerId,\n            broadcastAll: (rpcData) => {\n                return rpcMutex.exec(async () => {\n                    return rpc(\"/html_editor/bus_broadcast\", {\n                        model_name: modelName,\n                        field_name: fieldName,\n                        res_id: resId,\n                        bus_data: rpcData,\n                    });\n                });\n            },\n            onRequest: {\n                get_peer_metadata: this.getMetadata.bind(this),\n                get_missing_steps: (params) =>\n                    this.dependencies.collaboration.historyGetMissingSteps(params.requestPayload),\n                get_history_from_snapshot: () => this.getHistorySnapshot(),\n                get_collaborative_selection: () => this.getCurrentCollaborativeSelection(),\n                recover_document: (params) => {\n                    const { serverDocumentId, fromStepId } = params.requestPayload;\n                    if (\n                        !this.dependencies.collaboration.getBranchIds().includes(serverDocumentId)\n                    ) {\n                        return;\n                    }\n                    return {\n                        missingSteps: this.dependencies.collaboration.historyGetMissingSteps({\n                            fromStepId,\n                        }),\n                        snapshot: this.getHistorySnapshot(),\n                    };\n                },\n            },\n            onNotification: async (notification) => {\n                this.dispatchTo(\"collaboration_notification_handlers\", notification);\n                let { fromPeerId, notificationName, notificationPayload } = notification;\n                switch (notificationName) {\n                    case \"ptp_remove\":\n                        // todo: to implement\n                        // this.odooEditor.multiselectionRemove(notificationPayload);\n                        break;\n                    case \"ptp_disconnect\":\n                        this.ptp.removePeer(fromPeerId);\n                        // todo: to implement\n                        // this.odooEditor.multiselectionRemove(fromPeerId);\n                        break;\n                    case \"rtc_data_channel_open\": {\n                        fromPeerId = notificationPayload.connectionPeerId;\n                        const metadata = await this.requestPeer(\n                            fromPeerId,\n                            \"get_peer_metadata\",\n                            undefined,\n                            { transport: \"rtc\" }\n                        );\n                        if (metadata === REQUEST_ERROR) {\n                            return;\n                        }\n\n                        this.ptp.peersInfos[fromPeerId].metadata = metadata;\n\n                        if (!this.historySyncAtLeastOnce) {\n                            const localPeer = {\n                                id: this.config.collaboration.peerId,\n                                startTime: this.startCollaborationTime,\n                            };\n                            const remotePeer = {\n                                id: fromPeerId,\n                                startTime: metadata.startTime,\n                            };\n                            if (isPeerFirst(localPeer, remotePeer)) {\n                                this.historySyncAtLeastOnce = true;\n                                this.historySyncFinished = true;\n                            } else {\n                                this.resetCollabRequests();\n                                const response = await this.resetFromPeer(\n                                    fromPeerId,\n                                    this.lastCollaborationResetId\n                                );\n                                if (response === REQUEST_ERROR) {\n                                    return;\n                                }\n                            }\n                        } else {\n                            // Make both send their last step to each other to\n                            // ensure they are in sync.\n                            this.ptp.notifyAllPeers(\n                                \"oe_history_step\",\n                                this.dependencies.history.getHistorySteps().at(-1),\n                                { transport: \"rtc\" }\n                            );\n                            this.resetCollaborativeSelection(fromPeerId);\n                        }\n                        break;\n                    }\n                    case \"oe_history_step\":\n                        if (this.historySyncFinished) {\n                            this.dependencies.collaboration.onExternalHistorySteps([\n                                notificationPayload,\n                            ]);\n                        } else {\n                            this.historyStepsBuffer.push(notificationPayload);\n                        }\n                        break;\n                    case \"oe_history_set_selection\": {\n                        const peer = this.ptp.peersInfos[fromPeerId];\n                        if (!peer) {\n                            return;\n                        }\n                        const selection = notificationPayload;\n                        this.onExternalMultiselectionUpdate(selection);\n                        break;\n                    }\n                }\n            },\n        });\n    }\n    /**\n     * @param {string} peerId\n     */\n    getPeerMetadata(peerId) {\n        return this.ptp.peersInfos[peerId]?.metadata;\n    }\n    /**\n     * @param {CollaborationSelection} selection\n     */\n    onExternalMultiselectionUpdate(selection) {\n        this.dispatchTo(\"collaborative_selection_update_handlers\", selection);\n    }\n\n    async requestPeer(peerId, requestName, requestPayload, params) {\n        return this.ptp.requestPeer(peerId, requestName, requestPayload, params).catch((e) => {\n            if (e instanceof RequestError) {\n                return REQUEST_ERROR;\n            } else {\n                throw e;\n            }\n        });\n    }\n    getMetadata() {\n        const metadatas = {\n            startTime: this.startCollaborationTime,\n            peerName: user.name,\n        };\n        for (const cb of this.getResource(\"collaboration_peer_metadata_providers\")) {\n            Object.assign(metadatas, cb());\n        }\n        return metadatas;\n    }\n    /**\n     * Update the server document last step id and recover from a stale document\n     * if this peer does not have that step in its history.\n     */\n    onServerLastIdUpdate(last_step_id) {\n        this.serverLastStepId = last_step_id;\n        // Check if the current document is stale.\n        this.isDocumentStale = this.isLastDocumentStale();\n        if (this.isDocumentStale && this.ptpJoined) {\n            return this.recoverFromStaleDocument();\n        } else if (this.isDocumentStale && this.joiningPtp) {\n            // In case there is a stale document while a previous recovery is\n            // ongoing.\n            this.resetCollabRequests();\n            this.joinPeerToPeer();\n        }\n    }\n\n    joinPeerToPeer() {\n        this.editable.removeEventListener(\"focus\", this.joinPeerToPeer);\n        if (this.peerToPeerLoading) {\n            return this.peerToPeerLoading.then(async () => {\n                this.joiningPtp = true;\n                if (this.isDocumentStale) {\n                    const success = await this.resetFromServerAndResyncWithPeers();\n                    if (!success) {\n                        return;\n                    }\n                }\n                this.ptp.notifyAllPeers(\"ptp_join\");\n                this.joiningPtp = false;\n                this.ptpJoined = true;\n            });\n        }\n    }\n    isLastDocumentStale() {\n        if (!this.serverLastStepId) {\n            return false;\n        }\n        return !this.dependencies.collaboration.getBranchIds().includes(this.serverLastStepId);\n    }\n\n    /**\n     * Try to recover from a stale document.\n     *\n     * The strategy is:\n     *\n     * 1.  Try to get a converging document from the other peers.\n     *\n     * 1.1 By recovery from missing steps: it is the best possible case of\n     *     retrieval.\n     *\n     * 1.2 By recovery from snapshot: it reset the whole editor (destroying\n     *     changes and selection made by the user).\n     *\n     * 2. Reset from the server:\n     *    If the recovery from the other peers fails, reset from the server.\n     *\n     *    As we know we have a stale document, we need to reset it at least from\n     *    the server. We shouldn't wait too long for peers to respond because\n     *    the longer we wait for an unresponding peer, the longer a user can\n     *    edit a stale document.\n     *\n     *    The peers timeout is set to PTP_MAX_RECOVERY_TIME.\n     */\n    async recoverFromStaleDocument() {\n        return new Promise((resolve) => {\n            // 1. Try to recover a converging document from other peers.\n            const resetCollabCount = this.lastCollaborationResetId;\n\n            const allPeers = this.getPtpPeers().map((peer) => peer.id);\n\n            if (allPeers.length === 0) {\n                if (this.isDocumentStale) {\n                    this.showConflictDialog();\n                    resolve();\n                    return this.resetFromServerAndResyncWithPeers();\n                }\n            }\n\n            let hasRetrievalBudgetTimeout = false;\n            const snapshots = [];\n            let nbPendingResponses = allPeers.length;\n\n            const success = () => {\n                resolve();\n                clearTimeout(timeout);\n            };\n\n            for (const peerId of allPeers) {\n                this.requestPeer(\n                    peerId,\n                    \"recover_document\",\n                    {\n                        serverDocumentId: this.serverLastStepId,\n                        fromStepId: this.dependencies.collaboration.getBranchIds().at(-1),\n                    },\n                    { transport: \"rtc\" }\n                ).then((response) => {\n                    nbPendingResponses--;\n                    if (\n                        response === REQUEST_ERROR ||\n                        resetCollabCount !== this.lastCollaborationResetId ||\n                        hasRetrievalBudgetTimeout ||\n                        !response ||\n                        !this.isDocumentStale\n                    ) {\n                        if (nbPendingResponses <= 0) {\n                            processSnapshots();\n                        }\n                        return;\n                    }\n                    this.processMissingSteps(response.missingSteps);\n                    this.isDocumentStale = this.isLastDocumentStale();\n                    snapshots.push(response.snapshot);\n                    if (nbPendingResponses < 1) {\n                        processSnapshots();\n                    }\n                });\n            }\n\n            // Only process the snapshots after having received a response from all\n            // the peers or after PTP_MAX_RECOVERY_TIME in order to try to recover\n            // from missing steps.\n            const processSnapshots = async () => {\n                this.isDocumentStale = this.isLastDocumentStale();\n                if (!this.isDocumentStale) {\n                    return success();\n                }\n                if (snapshots[0]) {\n                    this.showConflictDialog();\n                }\n                for (const snapshot of snapshots) {\n                    this.applySnapshot(snapshot);\n                    this.isDocumentStale = this.isLastDocumentStale();\n                    // Prevent reseting from another snapshot if the document\n                    // converge.\n                    if (!this.isDocumentStale) {\n                        return success();\n                    }\n                }\n\n                // 2. If the document is still stale, try to recover from the server.\n                if (this.isDocumentStale) {\n                    this.showConflictDialog();\n                    await this.resetFromServerAndResyncWithPeers();\n                }\n\n                success();\n            };\n\n            // Wait PTP_MAX_RECOVERY_TIME to retrieve data from other peers to\n            // avoid reseting from the server if possible.\n            const timeout = setTimeout(() => {\n                if (resetCollabCount !== this.lastCollaborationResetId) {\n                    return;\n                }\n                hasRetrievalBudgetTimeout = true;\n                this.onRecoveryPeerTimeout(processSnapshots);\n            }, PTP_MAX_RECOVERY_TIME);\n        });\n    }\n\n    /**\n     * Get peer to peer peers.\n     */\n    getPtpPeers() {\n        const peers = Object.entries(this.ptp.peersInfos).map(([peerId, peerInfo]) => ({\n            id: peerId,\n            ...peerInfo,\n        }));\n        return peers.sort((a, b) => (isPeerFirst(a, b) ? -1 : 1));\n    }\n\n    getLastHistoryStepId(value) {\n        const matchId = value.match(/data-last-history-steps=\"[0-9,]*?([0-9]+)\"/);\n        return matchId && matchId[1];\n    }\n\n    resetCollabRequests() {\n        this.lastCollaborationResetId++;\n        // By aborting the current requests from ptp, we ensure that the ongoing\n        // `Wysiwyg.requestPeer` will return REQUEST_ERROR. Most requests that\n        // calls `Wysiwyg.requestPeer` might want to check if the response is\n        // REQUEST_ERROR.\n        this.ptp && this.ptp.abortCurrentRequests();\n    }\n    /**\n     * Reset the document from the server and resync with the peers.\n     */\n    async resetFromServerAndResyncWithPeers() {\n        let collaborationResetId = this.lastCollaborationResetId;\n        const record = await this.getCurrentRecord();\n        if (collaborationResetId !== this.lastCollaborationResetId) {\n            return;\n        }\n\n        const content =\n            record[this.config.collaboration.collaborationChannel.collaborationFieldName];\n        const lastHistoryId = content && this.getLastHistoryStepId(content);\n        // If a change was made in the document while retrieving it, the\n        // lastHistoryId will be different if the odoo bus did not have time to\n        // notify the user.\n        if (this.serverLastStepId !== lastHistoryId) {\n            // todo: instrument it to ensure it never happens\n            throw new Error(\n                \"Concurency detected while recovering from a stale document. The last history id of the server is different from the history id received by the html_field_write event.\"\n            );\n        }\n\n        this.isDocumentStale = false;\n        if (content) {\n            // content here is trusted\n            this.editable.innerHTML = content;\n        } else {\n            this.editable.replaceChildren(this.dependencies.baseContainer.createBaseContainer());\n        }\n        stripHistoryIds(this.editable);\n        this.dispatchTo(\"normalize_handlers\", this.editable);\n\n        this.dependencies.history.reset(content);\n\n        // After resetting from the server, try to resynchronise with a peer as\n        // if it was the first time connecting to a peer in order to retrieve a\n        // proper snapshot (e.g. This case could arise if we tried to recover\n        // from a peer but the timeout (PTP_MAX_RECOVERY_TIME) was reached\n        // before receiving a response).\n        this.historySyncAtLeastOnce = false;\n        this.resetCollabRequests();\n        collaborationResetId = this.lastCollaborationResetId;\n        this.startCollaborationTime = new Date().getTime();\n        await Promise.all(\n            this.getPtpPeers().map((peer) => {\n                // Reset from the fastest peer. The first peer to reset will set\n                // this.historySyncAtLeastOnce to true canceling the other peers\n                // resets.\n                return this.resetFromPeer(peer.id, collaborationResetId);\n            })\n        );\n        return true;\n    }\n    onReset(content) {\n        // This ID correspond to the peer that initiated the document and set\n        // the initial oid for all nodes in the tree. It is not the same as\n        // document that had a step id at some point. If a step comes from a\n        // different history, we should not apply it.\n        this.historyShareId = Math.floor(Math.random() * Math.pow(2, 52)).toString();\n\n        const lastStepId = content && this.getLastHistoryStepId(content);\n        if (lastStepId) {\n            this.dependencies.collaboration.setInitialBranchStepId(lastStepId);\n        }\n    }\n\n    /**\n     * Process missing steps received from a peer.\n     *\n     * @private\n     * @param {Array<Object>|-1} missingSteps\n     * @return {Promise<boolean>} true if missing steps have been processed\n     */\n    async processMissingSteps(missingSteps) {\n        // If missing steps === -1, it means that either:\n        // - the step.peerId has a stale document\n        // - the step.peerId has a snapshot and does not includes the step in\n        //   its history\n        // - if another share history id\n        //   - because the step.peerId has reset from the server and\n        //     step.peerId is not synced with this peer\n        //   - because the step.peerId is in a network partition\n        if (missingSteps === -1 || !missingSteps.length) {\n            return false;\n        }\n        this.dependencies.collaboration.onExternalHistorySteps(missingSteps);\n        return true;\n    }\n    applySnapshot(snapshot) {\n        const { steps, historyIds, historyShareId } = snapshot;\n        // If there is no serverLastStepId, it means that we use a document\n        // that is not versionned yet.\n        const isStaleDocument =\n            this.serverLastStepId && !historyIds.includes(this.serverLastStepId);\n        if (isStaleDocument) {\n            return;\n        }\n        this.historyShareId = historyShareId;\n        this.historySyncAtLeastOnce = true;\n        this.dependencies.collaboration.resetFromSteps(steps, historyIds);\n\n        // todo: ensure that if the selection was not in the editable before the\n        // reset, it remains where it was after applying the snapshot.\n        return true;\n    }\n\n    /**\n     * Callback for when the timeout PTP_MAX_RECOVERY_TIME fires.\n     *\n     * Used to be hooked in tests.\n     *\n     * @param {Function} processSnapshots The snapshot processing function.\n     */\n    async onRecoveryPeerTimeout(processSnapshots) {\n        processSnapshots();\n    }\n    showConflictDialog() {\n        // todo: implement conflict dialog\n        // if (this.conflictDialogOpened) {\n        //     return;\n        // }\n        // const content = markup(this.odooEditor.editable.cloneNode(true).outerHTML);\n        // this.conflictDialogOpened = true;\n        // this.env.services.dialog.add(ConflictDialog, {\n        //     content,\n        //     close: () => (this.conflictDialogOpened = false),\n        // });\n    }\n\n    getHistorySnapshot() {\n        return Object.assign({}, this.dependencies.collaboration.getSnapshotSteps(), {\n            historyShareId: this.historyShareId,\n        });\n    }\n\n    async resetFromPeer(fromPeerId, resetCollabCount) {\n        this.historySyncFinished = false;\n        this.historyStepsBuffer = [];\n        const snapshot = await this.requestPeer(\n            fromPeerId,\n            \"get_history_from_snapshot\",\n            undefined,\n            { transport: \"rtc\" }\n        );\n        if (snapshot === REQUEST_ERROR) {\n            return REQUEST_ERROR;\n        }\n        if (resetCollabCount !== this.lastCollaborationResetId) {\n            return;\n        }\n        // Ensure that the history hasn't been synced by another peer before\n        // this `get_history_from_snapshot` finished.\n        if (this.historySyncAtLeastOnce) {\n            return;\n        }\n        const selection = this.dependencies.selection.getEditableSelection();\n        let anchorNodeIndexPath = this._getNodeIndexPath(selection.anchorNode);\n        let anchorOffset = selection.anchorOffset;\n        if (selection.anchorNode === this.editable) {\n            anchorNodeIndexPath = this._getNodeIndexPath(this.editable.firstChild);\n            anchorOffset = 0;\n        }\n        const applied = this.applySnapshot(snapshot);\n        if (!applied) {\n            return;\n        }\n        const anchorNode = this._getNodeFromIndexPath(anchorNodeIndexPath);\n        if (\n            this.dependencies.selection.isSelectionInEditable({ anchorNode, focusNode: anchorNode })\n        ) {\n            this.dependencies.selection.setSelection({\n                anchorNode,\n                anchorOffset,\n            });\n        }\n        this.historySyncFinished = true;\n        // In case there are steps received in the meantime, process them.\n        if (this.historyStepsBuffer.length) {\n            this.dependencies.collaboration.onExternalHistorySteps(this.historyStepsBuffer);\n            this.historyStepsBuffer = [];\n        }\n        this.editable.dispatchEvent(new CustomEvent(\"onHistoryResetFromPeer\"));\n        this.resetCollaborativeSelection(fromPeerId);\n    }\n\n    async resetCollaborativeSelection(fromPeerId) {\n        const remoteSelection = await this.requestPeer(\n            fromPeerId,\n            \"get_collaborative_selection\",\n            undefined,\n            { transport: \"rtc\" }\n        );\n        if (remoteSelection === REQUEST_ERROR) {\n            return;\n        }\n        if (remoteSelection) {\n            this.onExternalMultiselectionUpdate(remoteSelection);\n        }\n    }\n    async onHistoryMissingParentStep({ step, fromStepId }) {\n        if (!this.ptp) {\n            return;\n        }\n        const missingSteps = await this.requestPeer(\n            step.peerId,\n            \"get_missing_steps\",\n            {\n                fromStepId: fromStepId,\n                toStepId: step.id,\n            },\n            { transport: \"rtc\" }\n        );\n        if (missingSteps === REQUEST_ERROR) {\n            return;\n        }\n        this.processMissingSteps(\n            Array.isArray(missingSteps) ? missingSteps.concat(step) : missingSteps\n        );\n    }\n    async getCurrentRecord() {\n        const [record] = await this.config.collaboration.ormService.read(\n            this.config.collaboration.collaborationChannel.collaborationModelName,\n            [this.config.collaboration.collaborationChannel.collaborationResId],\n            [this.config.collaboration.collaborationChannel.collaborationFieldName]\n        );\n        return record;\n    }\n    attachHistoryIds(editable) {\n        const historyIds = this.dependencies.collaboration.getBranchIds().join(\",\");\n        const firstChild = editable.children[0];\n        if (firstChild) {\n            firstChild.setAttribute(\"data-last-history-steps\", historyIds);\n        }\n    }\n\n    /**\n     * Generates the path to a node as an array of indices, relative to a given ancestor.\n     *\n     * @param {Node} node - The node to trace the path for.\n     * @returns {number[]} The path as an array of child indices.\n     */\n    _getNodeIndexPath(node) {\n        return [node, ...ancestors(node, this.editable)].map((ancestor) =>\n            childNodeIndex(ancestor)\n        );\n    }\n    /**\n     * Finds a node in the DOM based on a path of child indices.\n     *\n     * @param {number[]} indexPath - The path as an array of child indices.\n     * @returns {Node|undefined} The node at the specified path, or null if not found.\n     */\n    _getNodeFromIndexPath(indexPath) {\n        return indexPath.reduceRight(\n            (node, index) => node?.childNodes?.[index],\n            this.editable.parentElement\n        );\n    }\n}\n\n/**\n * Check wether peerA is before peerB.\n */\nfunction isPeerFirst(peerA, peerB) {\n    if (peerA.startTime === peerB.startTime) {\n        return peerA.id.localeCompare(peerB.id) === -1;\n    }\n    if (peerA.startTime === undefined || peerB.startTime === undefined) {\n        return Boolean(peerA.startTime);\n    } else {\n        return peerA.startTime < peerB.startTime;\n    }\n}\n\nexport function stripHistoryIds(element) {\n    element\n        .querySelectorAll(\"[data-last-history-steps]\")\n        .forEach((el) => el.removeAttribute(\"data-last-history-steps\"));\n}\n", "import { Plugin } from \"@html_editor/plugin\";\n\n// 60 seconds\nexport const HISTORY_SNAPSHOT_INTERVAL = 1000 * 60;\n// 10 seconds\nconst HISTORY_SNAPSHOT_BUFFER_TIME = 1000 * 10;\n\n/**\n * @typedef { Object } CollaborationPluginConfig\n * @property { string } peerId\n *\n * @typedef { import(\"../../core/history_plugin\").HistoryStep } HistoryStep\n */\n\n/**\n * @typedef { Object } CollaborationShared\n * @property { CollaborationPlugin['getBranchIds'] } getBranchIds\n * @property { CollaborationPlugin['getSnapshotSteps'] } getSnapshotSteps\n * @property { CollaborationPlugin['historyGetMissingSteps'] } historyGetMissingSteps\n * @property { CollaborationPlugin['onExternalHistorySteps'] } onExternalHistorySteps\n * @property { CollaborationPlugin['resetFromSteps'] } resetFromSteps\n * @property { CollaborationPlugin['setInitialBranchStepId'] } setInitialBranchStepId\n */\n\nexport class CollaborationPlugin extends Plugin {\n    static id = \"collaboration\";\n    static dependencies = [\"history\", \"selection\", \"sanitize\"];\n    resources = {\n        /** Handlers */\n        history_cleaned_handlers: this.onHistoryClean.bind(this),\n        history_reset_handlers: this.onHistoryReset.bind(this),\n        step_added_handlers: ({ step }) => this.onStepAdded(step),\n\n        /** Overrides */\n        set_attribute_overrides: this.setAttribute.bind(this),\n\n        history_step_processors: this.processHistoryStep.bind(this),\n        unreversible_step_predicates: this.isUnreversibleStep.bind(this),\n    };\n    static shared = [\n        \"getBranchIds\",\n        \"getSnapshotSteps\",\n        \"historyGetMissingSteps\",\n        \"onExternalHistorySteps\",\n        \"resetFromSteps\",\n        \"setInitialBranchStepId\",\n    ];\n\n    /** @type { CollaborationPluginConfig['peerId'] } */\n    peerId = null;\n\n    setup() {\n        this.peerId = this.config.collaboration.peerId;\n        if (!this.peerId) {\n            throw new Error(\"The collaboration plugin requires a peerId\");\n        }\n        this._snapshotInterval = setInterval(() => {\n            this.makeSnapshot();\n        }, HISTORY_SNAPSHOT_INTERVAL);\n    }\n\n    destroy() {\n        super.destroy();\n        clearInterval(this._snapshotInterval);\n        this._snapshotInterval = false;\n    }\n\n    onHistoryClean() {\n        this.branchStepIds = [];\n    }\n    onHistoryReset() {\n        const firstStep = this.dependencies.history.getHistorySteps()[0];\n        this.snapshots = [{ step: firstStep }];\n    }\n    /**\n     * @param {HistoryStep} step\n     */\n    isUnreversibleStep(step) {\n        return step.peerId !== this.peerId;\n    }\n    /**\n     * @param {Node} node\n     * @param {string} attributeName\n     * @param {string} attributeValue\n     */\n    setAttribute(node, attributeName, attributeValue) {\n        if (attributeValue) {\n            this.safeSetAttribute(node, attributeName, attributeValue);\n            return true;\n        }\n    }\n\n    /**\n     * Get all the history ids for the current history branch.\n     */\n    getBranchIds() {\n        const steps = this.dependencies.history.getHistorySteps();\n        return [this.initialBranchStepId].concat(this.branchStepIds).concat(steps.map((s) => s.id));\n    }\n    /**\n     * Safely set an attribute on a node.\n     * @param {HTMLElement} node\n     * @param {string} attributeName\n     * @param {string} attributeValue\n     */\n    safeSetAttribute(node, attributeName, attributeValue) {\n        const clone = this.document.createElement(node.tagName);\n        clone.setAttribute(attributeName, attributeValue);\n        this.dependencies.sanitize.sanitize(clone);\n        if (clone.hasAttribute(attributeName)) {\n            node.setAttribute(attributeName, clone.getAttribute(attributeName));\n        } else {\n            node.removeAttribute(attributeName);\n        }\n    }\n\n    /**\n     * Apply external steps coming from the collaboration.\n     *\n     * @param {Object} newSteps External steps to be applied\n     */\n    onExternalHistorySteps(newSteps) {\n        this.dependencies.history.disableObserver();\n        const selectionData = this.dependencies.selection.getSelectionData();\n\n        let stepIndex = 0;\n        const steps = this.dependencies.history.getHistorySteps();\n        for (const newStep of newSteps) {\n            // todo: add a test that no 2 history_missing_parent_step_handlers\n            // are called in same stack.\n            const insertIndex = this.getInsertStepIndex(steps, newStep);\n            if (typeof insertIndex === \"undefined\") {\n                continue;\n            }\n            this.dependencies.history.addExternalStep(newStep, insertIndex);\n            stepIndex++;\n        }\n\n        this.dependencies.history.enableObserver();\n        if (selectionData.documentSelectionIsInEditable) {\n            this.dependencies.selection.rectifySelection(selectionData.editableSelection);\n        }\n\n        this.dispatchTo(\"external_history_step_handlers\");\n\n        // todo: ensure that if the selection was not in the editable before the\n        // reset, it remains where it was after applying the snapshot.\n\n        if (stepIndex) {\n            this.config.onChange?.();\n        }\n    }\n\n    /**\n     * @param {HistoryStep[]} steps\n     * @param {HistoryStep} newStep\n     */\n    getInsertStepIndex(steps, newStep) {\n        let index = steps.length - 1;\n        while (index >= 0 && steps[index].id !== newStep.previousStepId) {\n            // Skip steps that are already in the list.\n            if (steps[index].id === newStep.id) {\n                return;\n            }\n            index--;\n        }\n\n        // When the previousStepId is not present in the steps it\n        // could be either:\n        // - the previousStepId is before a snapshot of the same history\n        // - the previousStepId has not been received because peers were\n        //   disconnected at that time\n        // - the previousStepId is in another history (in case two totally\n        //   differents `steps` (but it should not arise)).\n        if (index < 0) {\n            const historySteps = steps;\n            let index = historySteps.length - 1;\n            // Get the last known step that we are sure the missing step\n            // peer has. It could either be a step that has the same\n            // peerId or the first step.\n            while (index !== 0) {\n                if (historySteps[index].peerId === newStep.peerId) {\n                    break;\n                }\n                index--;\n            }\n            const fromStepId = historySteps[index].id;\n            this.dispatchTo(\"history_missing_parent_step_handlers\", {\n                step: newStep,\n                fromStepId: fromStepId,\n            });\n            return;\n        }\n\n        let concurentSteps = [];\n        index++;\n        while (index < steps.length) {\n            if (steps[index].previousStepId === newStep.previousStepId) {\n                if (steps[index].id.localeCompare(newStep.id) === 1) {\n                    break;\n                } else {\n                    concurentSteps = [steps[index].id];\n                }\n            } else {\n                if (concurentSteps.includes(steps[index].previousStepId)) {\n                    concurentSteps.push(steps[index].id);\n                } else {\n                    break;\n                }\n            }\n            index++;\n        }\n\n        return index;\n    }\n\n    /**\n     * @param {Object} params\n     * @param {string} params.fromStepId\n     * @param {string} [params.toStepId]\n     */\n    historyGetMissingSteps({ fromStepId, toStepId }) {\n        const steps = this.dependencies.history.getHistorySteps();\n        const fromIndex = steps.findIndex((x) => x.id === fromStepId);\n        const toIndex = toStepId ? steps.findIndex((x) => x.id === toStepId) : steps.length;\n        if (fromIndex === -1 || toIndex === -1) {\n            return -1;\n        }\n        return steps.slice(fromIndex + 1, toIndex);\n    }\n\n    getSnapshotSteps() {\n        const historySteps = this.dependencies.history.getHistorySteps();\n        // If the current snapshot has no time, it means that there is the no\n        // other snapshot that have been made (either it is the one created upon\n        // initialization or reseted by history's resetFromSteps).\n        if (!this.snapshots[0].time) {\n            return { steps: historySteps, historyIds: this.getBranchIds() };\n        }\n        const snapshotSteps = [];\n        let snapshot;\n        if (this.snapshots[0].time + HISTORY_SNAPSHOT_BUFFER_TIME < Date.now()) {\n            snapshot = this.snapshots[0];\n        } else {\n            // this.snapshots[1] has being created at least 1 minute ago\n            // (HISTORY_SNAPSHOT_INTERVAL) or it is the first step.\n            snapshot = this.snapshots[1];\n        }\n        let index = historySteps.length - 1;\n        while (historySteps[index].id !== snapshot.step.id) {\n            snapshotSteps.push(historySteps[index]);\n            index--;\n        }\n        snapshotSteps.push(snapshot.step);\n        snapshotSteps.reverse();\n\n        return { steps: snapshotSteps, historyIds: this.getBranchIds() };\n    }\n    setInitialBranchStepId(stepId) {\n        this.initialBranchStepId = stepId;\n    }\n    resetFromSteps(steps, branchStepIds) {\n        this.dependencies.selection.resetSelection();\n        this.dependencies.history.resetFromSteps(steps);\n        this.snapshots = [{ step: steps[0] }];\n        this.branchStepIds = branchStepIds;\n        this.dependencies.history.enableObserver();\n\n        // @todo @phoenix: test that the hint are proprely handeled\n        // this._handleCommandHint();\n        // @todo @phoenix: make the multiselection\n        // this.multiselectionRefresh();\n        // @todo @phoenix: check it is still relevant\n        // this.dispatchEvent(new Event(\"resetFromSteps\"));\n    }\n\n    makeSnapshot() {\n        const historyLength = this.dependencies.history.getHistorySteps().length;\n        if (!this.lastSnapshotLength || this.lastSnapshotLength < historyLength) {\n            this.lastSnapshotLength = historyLength;\n            const step = this.dependencies.history.makeSnapshotStep();\n            const snapshot = {\n                time: Date.now(),\n                step: step,\n            };\n            this.snapshots = [snapshot, this.snapshots[0]];\n        }\n    }\n\n    /**\n     * @param {HistoryStep} step\n     */\n    onStepAdded(step) {\n        step.peerId = this.peerId;\n        this.dispatchTo(\"collaboration_step_added_handlers\", step);\n    }\n    /**\n     * @param {HistoryStep} step\n     */\n    processHistoryStep(step) {\n        step.peerId = this.peerId;\n        return step;\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { closestBlock } from \"@html_editor/utils/blocks\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\n\n/**\n * @typedef {Object} SelectionInfo\n * @property {import(\"@html_editor/core/history_plugin\").SerializedSelection} selection\n * @property {string} color\n * @property {string} peerId\n * @property {string} peerName\n * @property {string} avatarPositionKey\n * @property {HTMLElement} avatarElement\n * @property {HTMLElement} avatarTargetElement\n */\n\nexport const AVATAR_SIZE = 25;\n\nexport class CollaborationSelectionAvatarPlugin extends Plugin {\n    static id = \"collaborationSelectionAvatar\";\n    static dependencies = [\"history\", \"position\", \"localOverlay\", \"collaborationOdoo\"];\n    resources = {\n        /** Handlers */\n        collaboration_notification_handlers: this.handleCollaborationNotification.bind(this),\n        external_history_step_handlers: this.refreshSelection.bind(this),\n        layout_geometry_change_handlers: this.refreshSelection.bind(this),\n        set_movable_element_handlers: this.disableAvatarForElement.bind(this),\n        unset_movable_element_handlers: this.enableAvatars.bind(this),\n        collaborative_selection_update_handlers: this.updateSelection.bind(this),\n\n        collaboration_peer_metadata_providers: () => ({ avatarUrl: this.avatarUrl }),\n    };\n\n    /** @type {Map<string, SelectionInfo>} */\n    selectionInfos = new Map();\n\n    setup() {\n        this.avatarOverlay = this.dependencies.localOverlay.makeLocalOverlay(\"oe-avatars-overlay\");\n        this.avatarsCountersOverlay = this.dependencies.localOverlay.makeLocalOverlay(\n            \"oe-avatars-counters-overlay\"\n        );\n        this.avatarUrl = `${\n            browser.location.origin\n        }/web/image?model=res.users&field=avatar_128&id=${encodeURIComponent(user.userId)}`;\n    }\n    handleCollaborationNotification({ notificationName, notificationPayload }) {\n        switch (notificationName) {\n            case \"ptp_remove\":\n                this.selectionInfos.delete(notificationPayload);\n                this.refreshSelection();\n        }\n    }\n\n    /**\n     * @param {import(\"./collaboration_odoo_plugin\").CollaborationSelection} selection\n     */\n    updateSelection(selection) {\n        /** @type {SelectionInfo} */\n        const savedSelection = this.selectionInfos.get(selection.peerId) || {};\n        const newSelection = Object.assign(savedSelection, selection);\n        this.selectionInfos.set(selection.peerId, newSelection);\n        this.drawPeerAvatar(newSelection);\n        this.updateAvatarCounters();\n    }\n    /**\n     * @param {SelectionInfo} selectionInfo\n     */\n    drawPeerAvatar(selectionInfo) {\n        const { selection, peerId } = selectionInfo;\n        const peerMetadata = this.dependencies.collaborationOdoo.getPeerMetadata(peerId);\n        if (!peerMetadata) {\n            return;\n        }\n        const { avatarUrl, peerName = _t(\"Anonymous\") } = peerMetadata;\n        const anchorNode = this.dependencies.history.getNodeById(selection.anchorNodeId);\n        const focusNode = this.dependencies.history.getNodeById(selection.focusNodeId);\n        if (!anchorNode || !focusNode || !anchorNode.isConnected || !focusNode.isConnected) {\n            return;\n        }\n        const anchorBlock = closestBlock(anchorNode);\n        if (!anchorBlock) {\n            return;\n        }\n\n        const containerRect = this.avatarOverlay.getBoundingClientRect();\n\n        // Draw user avatar.\n        let avatarElement = selectionInfo.avatarElement;\n        if (!avatarElement) {\n            avatarElement = this.document.createElement(\"div\");\n            avatarElement.className = \"oe-collaboration-caret-avatar\";\n            avatarElement.style.display = \"none\";\n            const image = this.document.createElement(\"img\");\n            avatarElement.append(image);\n            image.onload = () => avatarElement.style.removeProperty(\"display\");\n            image.setAttribute(\"src\", avatarUrl);\n            image.classList.add(\"o_object_fit_cover\");\n        }\n        // Avoid re-appending the element in the dom.\n        if (!avatarElement.parentElement) {\n            this.avatarOverlay.append(avatarElement);\n        }\n        // Make sure data is up to date.\n        selectionInfo.avatarElement = avatarElement;\n        selectionInfo.peerName = peerName;\n        selectionInfo.avatarTargetElement = anchorBlock;\n        this.selectionInfos.set(peerId, selectionInfo);\n\n        const anchorBlockRect = anchorBlock.getBoundingClientRect();\n        const top = anchorBlockRect.y - containerRect.y;\n        avatarElement.style.top = top + \"px\";\n        const closestList = closestElement(anchorNode, \"ul, ol\"); // Prevent overlap bullets.\n        const anchorX = closestList ? closestList.getBoundingClientRect().x : anchorBlockRect.x;\n        const left = anchorX - containerRect.x - AVATAR_SIZE;\n        avatarElement.style.left = left + \"px\";\n        selectionInfo.avatarPositionKey = `${left}|${top}`;\n    }\n    updateAvatarCounters() {\n        const avatarsOverlaps = {};\n        for (const info of this.selectionInfos.values()) {\n            const key = info.avatarPositionKey;\n            avatarsOverlaps[key] = avatarsOverlaps[key] || new Set();\n            avatarsOverlaps[key].add(info);\n        }\n\n        // Render avatars overlap.\n        this.avatarsCountersOverlay.replaceChildren();\n        for (const [overlapKey, infos] of Object.entries(avatarsOverlaps)) {\n            const size = infos.size;\n            if (size > 1) {\n                const [left, top] = overlapKey.split(\"|\").map((n) => parseInt(n, 10));\n                const div = document.createElement(\"div\");\n                div.className = \"oe-overlapping-counter\";\n                div.style.left = left + 10 + \"px\";\n                div.style.top = top + 10 + \"px\";\n                div.innerText = size;\n                this.avatarsCountersOverlay.append(div);\n            }\n        }\n    }\n    refreshSelection() {\n        if (!this.selectionInfos.size) {\n            this.avatarOverlay.replaceChildren();\n        }\n        this.avatarsCountersOverlay.replaceChildren();\n        for (const selection of this.selectionInfos.values()) {\n            this.drawPeerAvatar(selection);\n        }\n        this.updateAvatarCounters();\n    }\n\n    disableAvatarForElement(element) {\n        this.enableAvatars();\n        for (const info of this.selectionInfos.values()) {\n            if (info.avatarTargetElement === element) {\n                if (!info.avatarElement.classList.contains(\"invisible\")) {\n                    info.avatarElement.classList.add(\"invisible\");\n                }\n            }\n        }\n    }\n    enableAvatars() {\n        for (const element of this.avatarOverlay.querySelectorAll(\n            \".oe-collaboration-caret-avatar.invisible\"\n        )) {\n            element.classList.remove(\"invisible\");\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport {\n    getDeepestPosition,\n    isProtected,\n    isProtecting,\n    isUnprotecting,\n} from \"@html_editor/utils/dom_info\";\nimport { childNodes } from \"@html_editor/utils/dom_traversal\";\nimport { DIRECTIONS } from \"@html_editor/utils/position\";\nimport { getCursorDirection } from \"@html_editor/utils/selection\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class CollaborationSelectionPlugin extends Plugin {\n    static id = \"collaborationSelection\";\n    static dependencies = [\"history\", \"collaborationOdoo\", \"position\", \"localOverlay\"];\n    resources = {\n        /** Handlers */\n        collaboration_notification_handlers: this.handleCollaborationNotification.bind(this),\n        layout_geometry_change_handlers: this.refreshSelection.bind(this),\n        collaborative_selection_update_handlers: this.updateSelection.bind(this),\n\n        collaboration_peer_metadata_providers: () => ({ selectionColor: this.selectionColor }),\n    };\n    selectionInfos = new Map();\n\n    setup() {\n        this.selectionOverlay =\n            this.dependencies.localOverlay.makeLocalOverlay(\"oe-selections-container\");\n        this.selectionColor = `hsl(${(Math.random() * 360).toFixed(0)}, 75%, 50%)`;\n    }\n    handleCollaborationNotification({ notificationName, notificationPayload }) {\n        switch (notificationName) {\n            case \"ptp_remove\":\n                this.multiselectionRemove(notificationPayload);\n                this.selectionInfos.delete(notificationPayload);\n                break;\n        }\n    }\n    /**\n     * @param {import(\"./collaboration_odoo_plugin\").CollaborationSelection} selection\n     */\n    updateSelection(selection) {\n        this.selectionInfos.set(selection.peerId, selection);\n        this.drawPeerSelection(selection);\n    }\n    /**\n     * @param {import(\"./collaboration_odoo_plugin\").CollaborationSelection} selection\n     */\n    drawPeerSelection({ selection, peerId }) {\n        const peerMetadata = this.dependencies.collaborationOdoo.getPeerMetadata(peerId);\n        if (!peerMetadata) {\n            return;\n        }\n        const { selectionColor, peerName = _t(\"Anonymous\") } = peerMetadata;\n        this.multiselectionRemove(peerId);\n        let clientRects;\n\n        let anchorNode = this.dependencies.history.getNodeById(selection.anchorNodeId);\n        let focusNode = this.dependencies.history.getNodeById(selection.focusNodeId);\n        let anchorOffset = selection.anchorOffset;\n        let focusOffset = selection.focusOffset;\n        if (!anchorNode || !focusNode) {\n            anchorNode = this.editable.children[0];\n            focusNode = this.editable.children[0];\n            anchorOffset = 0;\n            focusOffset = 0;\n        }\n        const anchorTarget = childNodes(anchorNode).at(anchorOffset);\n        const focusTarget = childNodes(focusNode).at(focusOffset);\n        const protectionCheck = (node) =>\n            isProtecting(node) || (isProtected(node) && !isUnprotecting(node));\n        if (protectionCheck(anchorTarget) || protectionCheck(focusTarget)) {\n            // TODO @phoenix, TODO ABD: better handle collaborative selection\n            // on protected elements.\n            return;\n        }\n        if (anchorNode.isConnected && focusNode.isConnected) {\n            [anchorNode, anchorOffset] = getDeepestPosition(anchorNode, anchorOffset);\n            [focusNode, focusOffset] = getDeepestPosition(focusNode, focusOffset);\n        } else {\n            // todo: We should not be able to get here, this fixes multiples\n            // issues where we temporarily try to draw a an impossible\n            // selection. We should investigate the root cause of this issue.\n            anchorNode = this.editable.children[0];\n            focusNode = this.editable.children[0];\n            anchorOffset = 0;\n            focusOffset = 0;\n        }\n\n        const direction = getCursorDirection(anchorNode, anchorOffset, focusNode, focusOffset);\n        const range = new Range();\n        try {\n            if (direction === DIRECTIONS.RIGHT) {\n                range.setStart(anchorNode, anchorOffset);\n                range.setEnd(focusNode, focusOffset);\n            } else {\n                range.setStart(focusNode, focusOffset);\n                range.setEnd(anchorNode, anchorOffset);\n            }\n\n            clientRects = Array.from(range.getClientRects());\n        } catch {\n            // Changes in the dom might prevent the range to be instantiated\n            // (because of a removed node for example), in which case we ignore\n            // the range.\n            clientRects = [];\n        }\n        if (!clientRects.length) {\n            return;\n        }\n\n        // Draw rects (in case the selection is not collapsed).\n        const containerRect = this.selectionOverlay.getBoundingClientRect();\n        const indicators = clientRects.map(({ x, y, width, height }) => {\n            const rectElement = this.document.createElement(\"div\");\n            rectElement.style = `\n                position: absolute;\n                top: ${y - containerRect.y}px;\n                left: ${x - containerRect.x}px;\n                width: ${width}px;\n                height: ${height}px;\n                background-color: ${selectionColor};\n                opacity: 0.25;\n                pointer-events: none;\n            `;\n            rectElement.setAttribute(\"data-selection-peer-id\", peerId);\n            return rectElement;\n        });\n\n        // Draw carret.\n        const caretElement = this.document.createElement(\"div\");\n        caretElement.style = `border-left: 2px solid ${selectionColor}; position: absolute;`;\n        caretElement.setAttribute(\"data-selection-peer-id\", peerId);\n        caretElement.className = \"oe-collaboration-caret\";\n\n        // Draw carret top square.\n        const caretTopSquare = this.document.createElement(\"div\");\n        caretTopSquare.className = \"oe-collaboration-caret-top-square\";\n        caretTopSquare.style[\"background-color\"] = selectionColor;\n        caretTopSquare.setAttribute(\"data-peer-name\", peerName);\n        caretElement.append(caretTopSquare);\n\n        if (direction === DIRECTIONS.LEFT) {\n            const rect = clientRects[0];\n            caretElement.style.height = `${rect.height * 1.2}px`;\n            caretElement.style.top = `${rect.y - containerRect.y}px`;\n            caretElement.style.left = `${rect.x - containerRect.x}px`;\n        } else {\n            const rect = clientRects.at(-1);\n            caretElement.style.height = `${rect.height * 1.2}px`;\n            caretElement.style.top = `${rect.y - containerRect.y}px`;\n            caretElement.style.left = `${rect.right - containerRect.x}px`;\n        }\n        this.selectionOverlay.append(caretElement, ...indicators);\n    }\n\n    multiselectionRemove(peerId) {\n        const elements = this.selectionOverlay.querySelectorAll(\n            `[data-selection-peer-id=\"${peerId}\"]`\n        );\n        for (const element of elements) {\n            element.remove();\n        }\n    }\n    refreshSelection() {\n        this.selectionOverlay.replaceChildren();\n        for (const selection of this.selectionInfos.values()) {\n            this.drawPeerSelection(selection);\n        }\n    }\n}\n", "import { MediaDialog } from \"../main/media/media_dialog/media_dialog\";\n\nexport class CustomMediaDialog extends MediaDialog {\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        if (this.state.activeTab == \"IMAGES\") {\n            const attachments = this.selectedMedia[this.state.activeTab];\n            const preloadedAttachments = attachments.filter((attachment) => attachment.res_model);\n            this.selectedMedia[this.state.activeTab] = attachments.filter(\n                (attachment) => !preloadedAttachments.includes(attachment)\n            );\n            if (this.selectedMedia[this.state.activeTab].length > 0) {\n                await super.save();\n                const newAttachments = this.selectedMedia[this.state.activeTab];\n                this.props.imageSave(newAttachments);\n            }\n            if (preloadedAttachments.length) {\n                this.props.imageSave(preloadedAttachments);\n            }\n        } else {\n            this.props.videoSave(this.selectedMedia[this.state.activeTab]);\n        }\n        this.props.close();\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { DynamicPlaceholderPopover } from \"@web/views/fields/dynamic_placeholder_popover\";\nimport { withSequence } from \"@html_editor/utils/resource\";\n\n/**\n * @typedef {Object} DynamicPlaceholderShared\n * @property {DynamicPlaceholderPlugin['updateDphDefaultModel']} updateDphDefaultModel\n */\n\nexport class DynamicPlaceholderPlugin extends Plugin {\n    static id = \"dynamicPlaceholder\";\n    static dependencies = [\"overlay\", \"selection\", \"history\", \"dom\"];\n    static shared = [\"updateDphDefaultModel\"];\n    resources = {\n        user_commands: [\n            {\n                id: \"openDynamicPlaceholder\",\n                title: _t(\"Dynamic Placeholder\"),\n                description: _t(\"Insert a field\"),\n                icon: \"fa-hashtag\",\n                run: (params = {}) => {\n                    return this.open(params.resModel || this.defaultResModel);\n                },\n            },\n        ],\n        powerbox_categories: withSequence(60, {\n            id: \"marketing_tools\",\n            name: _t(\"Marketing Tools\"),\n        }),\n        powerbox_items: {\n            categoryId: \"marketing_tools\",\n            commandId: \"openDynamicPlaceholder\",\n        },\n        power_buttons: { commandId: \"openDynamicPlaceholder\" },\n    };\n    setup() {\n        this.defaultResModel = this.config.dynamicPlaceholderResModel;\n\n        /** @type {import(\"@html_editor/core/overlay_plugin\").Overlay} */\n        this.overlay = this.dependencies.overlay.createOverlay(DynamicPlaceholderPopover, {\n            hasAutofocus: true,\n            className: \"popover\",\n        });\n    }\n\n    /**\n     * @param {string} resModel\n     */\n    updateDphDefaultModel(resModel) {\n        this.defaultResModel = resModel;\n    }\n\n    /**\n     * @param {string} resModel\n     */\n    open(resModel) {\n        if (!resModel) {\n            return this.services.notification.add(\n                _t(\"You need to select a model before opening the dynamic placeholder selector.\"),\n                { type: \"danger\" }\n            );\n        }\n        this.overlay.open({\n            props: {\n                close: this.onClose.bind(this),\n                validate: this.onValidate.bind(this),\n                resModel: resModel,\n            },\n        });\n    }\n\n    /**\n     * @param {string} chain\n     * @param {string} defaultValue\n     */\n    onValidate(chain, defaultValue) {\n        if (!chain) {\n            return;\n        }\n\n        const t = document.createElement(\"T\");\n        t.setAttribute(\"t-out\", `object.${chain}`);\n        if (defaultValue?.length) {\n            t.innerText = defaultValue;\n        }\n\n        this.dependencies.dom.insert(t);\n        this.dependencies.history.addStep();\n    }\n\n    onClose() {\n        this.overlay.close();\n        this.dependencies.selection.focusEditable();\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { memoize } from \"@web/core/utils/functions\";\n\n/**\n * This plugin is responsible with providing the API to manipulate/insert\n * sub components in an editor.\n */\nexport class EmbeddedComponentPlugin extends Plugin {\n    static id = \"embeddedComponents\";\n    static dependencies = [\"history\", \"protectedNode\"];\n    resources = {\n        /** Handlers */\n        normalize_handlers: this.normalize.bind(this),\n        clean_for_save_handlers: ({ root }) => this.cleanForSave(root),\n        attribute_change_handlers: this.onChangeAttribute.bind(this),\n        restore_savepoint_handlers: () => this.handleComponents(this.editable),\n        history_reset_handlers: () => this.handleComponents(this.editable),\n        history_reset_from_steps_handlers: () => this.handleComponents(this.editable),\n        step_added_handlers: ({ stepCommonAncestor }) => this.handleComponents(stepCommonAncestor),\n        external_step_added_handlers: () => this.handleComponents(this.editable),\n\n        serializable_descendants_processors: this.processDescendantsToSerialize.bind(this),\n        attribute_change_processors: this.onChangeAttribute.bind(this),\n        savable_mutation_record_predicates: this.isMutationRecordSavable.bind(this),\n    };\n\n    setup() {\n        this.components = new Set();\n        // map from node to component info\n        this.nodeMap = new WeakMap();\n        this.app = this.config.embeddedComponentInfo.app;\n        this.env = this.config.embeddedComponentInfo.env;\n        this.hostToStateChangeManagerMap = new WeakMap();\n        this.embeddedComponents = memoize((embeddedComponents = []) => {\n            const result = {};\n            for (const embedding of embeddedComponents) {\n                // TODO ABD: Any embedding with the same name as another will overwrite it.\n                // File currently relies on this system. Change it ?\n                result[embedding.name] = embedding;\n            }\n            return result;\n        });\n        // First mount is done during history_reset_handlers which happens\n        // when start_edition_handlers are called.\n    }\n\n    isMutationRecordSavable(record) {\n        const info = this.nodeMap.get(record.target);\n        if (\n            info &&\n            record.type === \"attributes\" &&\n            record.attributeName === \"data-embedded-props\"\n        ) {\n            // This attribute is determined independently for each user\n            // through `data-embedded-state` attribute mutations.\n            return false;\n        }\n        return true;\n    }\n\n    processDescendantsToSerialize(elem, serializableDescendants) {\n        const embedding = this.getEmbedding(elem);\n        if (!embedding) {\n            return serializableDescendants;\n        }\n        return Object.values(embedding.getEditableDescendants?.(elem) || {});\n    }\n\n    handleComponents(elem) {\n        this.destroyRemovedComponents([...this.components]);\n        this.forEachEmbeddedComponentHost(elem, (host, embedding) => {\n            const info = this.nodeMap.get(host);\n            if (!info) {\n                this.mountComponent(host, embedding);\n            }\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.getResource(\"embedded_components\"))[\n            host.dataset.embedded\n        ];\n    }\n\n    /**\n     * Apply an embedded state change received from `data-embedded-state`\n     * attribute. In some cases (undo/redo/revertStepsUntil history operations),\n     * the attribute has to be set to a new value, computed by the\n     * stateChangeManager.\n     *\n     * @param {Object} attributeChange @see HistoryPlugin\n     * @param { Object } options\n     * @param { boolean } options.forNewStep whether the mutation is being used\n     *        to create a new step\n     * @returns {string} new attribute value to set on the node, which might be\n     *        unchanged\n     */\n    onChangeAttribute(attributeChange, { forNewStep = false } = {}) {\n        const attributeValue = attributeChange.value;\n        let newAttributeValue;\n        if (attributeChange.attributeName === \"data-embedded-state\") {\n            const attrState = attributeChange.reverse\n                ? attributeChange.oldValue\n                : attributeChange.value;\n            const stateChangeManager = this.getStateChangeManager(attributeChange.target);\n            if (stateChangeManager) {\n                // onStateChanged returns undefined if no change is needed for\n                // the attribute value\n                newAttributeValue = stateChangeManager.onStateChanged(attrState, {\n                    reverse: attributeChange.reverse,\n                    forNewStep,\n                });\n            }\n        }\n        return newAttributeValue || attributeValue;\n    }\n\n    getStateChangeManager(host) {\n        const embedding = this.getEmbedding(host);\n        if (!(\"getStateChangeManager\" in embedding)) {\n            return null;\n        }\n        if (!this.hostToStateChangeManagerMap.has(host)) {\n            const config = {\n                host,\n                commitStateChanges: () => this.dependencies.history.addStep(),\n            };\n            const stateChangeManager = embedding.getStateChangeManager(config);\n            stateChangeManager.setup();\n            this.hostToStateChangeManagerMap.set(host, stateChangeManager);\n        }\n        return this.hostToStateChangeManagerMap.get(host);\n    }\n\n    mountComponent(\n        host,\n        { Component, getEditableDescendants, getProps, name, getStateChangeManager }\n    ) {\n        const props = getProps?.(host) || {};\n        const env = Object.create(this.env);\n        if (getStateChangeManager) {\n            env.getStateChangeManager = this.getStateChangeManager.bind(this);\n        }\n        if (getEditableDescendants) {\n            env.getEditableDescendants = getEditableDescendants;\n        }\n        this.dispatchTo(\"mount_component_handlers\", { name, env, props });\n        const root = this.app.createRoot(Component, {\n            props,\n            env,\n        });\n        root.mount(host);\n        // Patch mount fiber to hook into the exact call stack where root is\n        // mounted (but before). This will remove host children synchronously\n        // just before adding the root rendered html.\n        const fiber = root.node.fiber;\n        const fiberComplete = fiber.complete;\n        fiber.complete = function () {\n            host.replaceChildren();\n            fiberComplete.call(this);\n        };\n        const info = {\n            root,\n            host,\n        };\n        this.components.add(info);\n        this.nodeMap.set(host, info);\n    }\n\n    destroyRemovedComponents(infos) {\n        // Avoid registering mutations if removed hosts are handled in\n        // the same microtask as when they were removed.\n        this.dependencies.history.disableObserver();\n        for (const info of infos) {\n            if (!this.editable.contains(info.host)) {\n                const host = info.host;\n                const display = host.style.display;\n                const parentNode = host.parentNode;\n                const clone = host.cloneNode(false);\n                if (parentNode) {\n                    parentNode.replaceChild(clone, host);\n                }\n                host.style.display = \"none\";\n                this.editable.after(host);\n                this.destroyComponent(info);\n                if (parentNode) {\n                    parentNode.replaceChild(host, clone);\n                } else {\n                    host.remove();\n                }\n                host.style.display = display;\n                if (!host.getAttribute(\"style\")) {\n                    host.removeAttribute(\"style\");\n                }\n            }\n        }\n        this.dependencies.history.enableObserver();\n    }\n\n    deepDestroyComponent({ host }) {\n        const removed = [];\n        this.forEachEmbeddedComponentHost(host, (containedHost) => {\n            const info = this.nodeMap.get(containedHost);\n            if (info) {\n                if (this.editable.contains(containedHost)) {\n                    this.destroyComponent(info);\n                } else {\n                    removed.push(info);\n                }\n            }\n        });\n        this.destroyRemovedComponents(removed);\n    }\n\n    /**\n     * Should not be called directly as it will not handle recursivity and\n     * removed components @see deepDestroyComponent\n     */\n    destroyComponent({ root, host }) {\n        const { getEditableDescendants } = this.getEmbedding(host);\n        const editableDescendants = getEditableDescendants?.(host) || {};\n        root.destroy();\n        this.components.delete(arguments[0]);\n        this.nodeMap.delete(host);\n        host.append(...Object.values(editableDescendants));\n    }\n\n    destroy() {\n        super.destroy();\n        for (const info of [...this.components]) {\n            if (this.components.has(info)) {\n                this.deepDestroyComponent(info);\n            }\n        }\n    }\n\n    normalize(elem) {\n        this.forEachEmbeddedComponentHost(elem, (host, { getEditableDescendants }) => {\n            this.dependencies.protectedNode.setProtectingNode(host, true);\n            const editableDescendants = getEditableDescendants?.(host) || {};\n            for (const editableDescendant of Object.values(editableDescendants)) {\n                this.dependencies.protectedNode.setProtectingNode(editableDescendant, false);\n            }\n        });\n    }\n\n    cleanForSave(clone) {\n        this.forEachEmbeddedComponentHost(clone, (host, { getEditableDescendants }) => {\n            // In this case, host is a cloned element, there is no OWL root\n            // attached to it.\n            const editableDescendants = getEditableDescendants?.(host) || {};\n            host.replaceChildren();\n            for (const editableDescendant of Object.values(editableDescendants)) {\n                delete editableDescendant.dataset.oeProtected;\n                host.append(editableDescendant);\n            }\n            delete host.dataset.oeProtected;\n            delete host.dataset.embeddedState;\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.commitChanges notify the host that we can commit\n     *                                        changes\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        if (!this.previousEmbeddedState) {\n            // If there is no previousEmbeddedState, it means that no\n            // effective change was performed, so there is nothing to commit.\n            return;\n        }\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.commitStateChanges();\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 {\n    applyObjectPropertyDifference,\n    getEmbeddedProps,\n    StateChangeManager,\n    useEmbeddedState,\n} from \"@html_editor/others/embedded_component_utils\";\nimport { useEffect, useRef, useState } from \"@odoo/owl\";\nimport { ReadonlyEmbeddedFileComponent } from \"@html_editor/others/embedded_components/core/file/readonly_file\";\n\nexport class EmbeddedFileComponent extends ReadonlyEmbeddedFileComponent {\n    static template = \"html_editor.EmbeddedFile\";\n\n    setup() {\n        super.setup();\n        // override the state by an embedded state.\n        this.state = useEmbeddedState(this.props.host);\n        this.fileModel.state = this.state;\n        this.localState = useState({\n            editFileName: false,\n        });\n        this.nameInput = useRef(\"nameInput\");\n        useEffect(\n            () => {\n                if (this.localState.editFileName) {\n                    this.nameInput.el.focus();\n                    this.nameInput.el.select();\n                }\n            },\n            () => [this.localState.editFileName]\n        );\n    }\n\n    onBlurNameInput(ev) {\n        this.localState.editFileName = false;\n        this.renameFile();\n    }\n\n    onFocusFileName(ev) {\n        this.localState.editFileName = true;\n    }\n\n    onKeydownNameInput(ev) {\n        if (ev.key !== \"Enter\") {\n            return;\n        } else {\n            ev.preventDefault();\n        }\n        if (this.renameFile()) {\n            this.localState.editFileName = false;\n            this.env.editorShared?.setSelectionAfter(this.props.host);\n        }\n    }\n\n    renameFile() {\n        let newName = this.nameInput.el.value;\n        if (!newName.length) {\n            return false;\n        }\n        if (newName === this.fileModel.filename) {\n            return true;\n        }\n        // filename is the name of the file as written in the editor by the\n        // user. It does not necessarily have the file extension.\n        this.fileModel.filename = newName;\n        if (this.fileModel.extension) {\n            const pattern = new RegExp(`\\\\.${this.fileModel.extension}$`, \"i\");\n            if (!newName.match(pattern)) {\n                newName += `.${this.fileModel.extension}`;\n            }\n        }\n        // name is the full name of the file (always with extension)\n        // and is used as the url queryParam when downloading it.\n        this.fileModel.name = newName;\n        return true;\n    }\n}\n\nexport const fileEmbedding = {\n    name: \"file\",\n    Component: EmbeddedFileComponent,\n    getProps: (host) => {\n        return { host, ...getEmbeddedProps(host) };\n    },\n    getStateChangeManager: (config) => {\n        return new StateChangeManager(\n            Object.assign(config, {\n                propertyUpdater: {\n                    fileData: (state, previous, next) => {\n                        applyObjectPropertyDifference(\n                            state,\n                            \"fileData\",\n                            previous.fileData,\n                            next.fileData\n                        );\n                    },\n                },\n            })\n        );\n    },\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 { _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     * Method no longer used, kept for compatibility (stable policy).\n     * To be removed in master.\n     *\n     * @param {Event} ev\n     */\n    async onClickDownload(ev) {\n        ev.preventDefault();\n        ev.stopPropagation();\n        await this.download();\n    }\n\n    /**\n     * This 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     */\n    async download() {\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        } else {\n            this.download();\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 { fileEmbedding } from \"@html_editor/others/embedded_components/backend/file/file\";\nimport { readonlyFileEmbedding } from \"@html_editor/others/embedded_components/core/file/readonly_file\";\nimport {\n    readonlyTableOfContentEmbedding,\n    tableOfContentEmbedding,\n} from \"@html_editor/others/embedded_components/core/table_of_content/table_of_content\";\nimport { videoEmbedding } from \"@html_editor/others/embedded_components/core/video/video\";\n\nexport const MAIN_EMBEDDINGS = [fileEmbedding, tableOfContentEmbedding, videoEmbedding];\n\nexport const READONLY_MAIN_EMBEDDINGS = [\n    readonlyFileEmbedding,\n    readonlyTableOfContentEmbedding,\n    videoEmbedding,\n];\n", "import { DocumentSelector } from \"@html_editor/main/media/media_dialog/document_selector\";\nimport { renderToElement } from \"@web/core/utils/render\";\n\n/**\n * Override the @see DocumentSelector to render the uploaded file as embedded\n * component with editable file name and previewable file.\n */\nexport class EmbeddedFileDocumentsSelector extends DocumentSelector {\n    static mediaSpecificClasses = [];\n\n    /** @override */\n    static async renderFileElement(attachment) {\n        return renderEmbeddedFileBox(attachment);\n    }\n}\n\n/**\n * @param {Object} attachment\n * @returns {Element}\n */\nexport function renderEmbeddedFileBox(attachment) {\n    const dotSplit = attachment.name.split(\".\");\n    const extension = dotSplit.length > 1 ? dotSplit.pop() : undefined;\n    const fileData = {\n        access_token: attachment.access_token,\n        checksum: attachment.checksum,\n        extension,\n        filename: attachment.name,\n        id: attachment.id,\n        mimetype: attachment.mimetype,\n        name: attachment.name,\n        type: attachment.type,\n        url: attachment.url || \"\",\n    };\n    return renderToElement(\"html_editor.EmbeddedFileBlueprint\", {\n        embeddedProps: JSON.stringify({ fileData }),\n    });\n}\n", "import { nextLeaf } from \"@html_editor/utils/dom_info\";\nimport { isBlock } from \"@html_editor/utils/blocks\";\nimport {\n    EmbeddedFileDocumentsSelector,\n    renderEmbeddedFileBox,\n} from \"./embedded_file_documents_selector\";\nimport { FilePlugin } from \"@html_editor/main/media/file_plugin\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\n\n/**\n * This plugin is meant to replace the File plugin.\n */\nexport class EmbeddedFilePlugin extends FilePlugin {\n    static id = \"embeddedFile\";\n    static dependencies = [...super.dependencies, \"embeddedComponents\", \"selection\"];\n\n    // Extends the base class resources\n    resources = {\n        ...this.resources,\n        mount_component_handlers: this.setupNewFile.bind(this),\n    };\n\n    /** @override */\n    renderDownloadBox(attachment) {\n        return renderEmbeddedFileBox(attachment);\n    }\n\n    /** @override */\n    isUploadCommandAvailable({ anchorNode }) {\n        return (\n            super.isUploadCommandAvailable() &&\n            !closestElement(anchorNode, \"[data-embedded='clipboard']\")\n        );\n    }\n\n    /** @override */\n    get componentForMediaDialog() {\n        return EmbeddedFileDocumentsSelector;\n    }\n\n    setupNewFile({ name, env }) {\n        if (name === \"file\") {\n            Object.assign(env, {\n                editorShared: {\n                    setSelectionAfter: (host) => {\n                        try {\n                            const leaf = nextLeaf(host, this.editable);\n                            if (!leaf) {\n                                return;\n                            }\n                            const leafEl = isBlock(leaf) ? leaf : leaf.parentElement;\n                            if (isBlock(leafEl) && leafEl.isContentEditable) {\n                                this.dependencies.selection.setSelection({\n                                    anchorNode: leafEl,\n                                    anchorOffset: 0,\n                                });\n                            }\n                        } catch {\n                            return;\n                        }\n                    },\n                },\n            });\n        }\n    }\n}\n", "/**\n * This file is no longer used, and is kept for compatibility (stable policy).\n * To be removed in master.\n */\n\nimport { Plugin } from \"@html_editor/plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { FileMediaDialog } from \"@html_editor/main/media/media_dialog/file_media_dialog\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { nextLeaf } from \"@html_editor/utils/dom_info\";\nimport { isBlock } from \"@html_editor/utils/blocks\";\n\nexport class FilePlugin extends Plugin {\n    static id = \"file\";\n    static dependencies = [\"embeddedComponents\", \"dom\", \"selection\", \"history\"];\n    resources = {\n        user_commands: [\n            {\n                id: \"openMediaDialog\",\n                title: _t(\"File\"),\n                description: _t(\"Upload a file\"),\n                icon: \"fa-file\",\n                isAvailable: (selection) => {\n                    return (\n                        !this.config.disableFile &&\n                        !closestElement(selection.anchorNode, \"[data-embedded='clipboard']\")\n                    );\n                },\n                run: () => {\n                    this.openMediaDialog({\n                        noVideos: true,\n                        noImages: true,\n                        noIcons: true,\n                    });\n                },\n            },\n        ],\n        powerbox_items: [\n            {\n                categoryId: \"media\",\n                commandId: \"openMediaDialog\",\n            },\n        ],\n        mount_component_handlers: this.setupNewFile.bind(this),\n    };\n\n    get recordInfo() {\n        return this.config.getRecordInfo ? this.config.getRecordInfo() : {};\n    }\n\n    openMediaDialog(params = {}) {\n        const selection = this.dependencies.selection.getEditableSelection();\n        const restoreSelection = () => {\n            this.dependencies.selection.setSelection(selection);\n        };\n        const { resModel, resId, field, type } = this.recordInfo;\n        this.services.dialog.add(FileMediaDialog, {\n            resModel,\n            resId,\n            useMediaLibrary: !!(\n                field &&\n                ((resModel === \"ir.ui.view\" && field === \"arch\") || type === \"html\")\n            ), // @todo @phoenix: should be removed and moved to config.mediaModalParams\n            save: (element) => {\n                this.onSaveMediaDialog(element, { restoreSelection });\n            },\n            close: restoreSelection,\n            onAttachmentChange: this.config.onAttachmentChange || (() => {}),\n            noVideos: !!this.config.disableVideo,\n            noImages: !!this.config.disableImage,\n            ...this.config.mediaModalParams,\n            ...params,\n        });\n    }\n\n    onSaveMediaDialog(element, { restoreSelection }) {\n        restoreSelection();\n        this.dependencies.dom.insert(element);\n        this.dependencies.history.addStep();\n    }\n\n    setupNewFile({ name, env }) {\n        if (name === \"file\") {\n            Object.assign(env, {\n                editorShared: {\n                    setSelectionAfter: (host) => {\n                        try {\n                            const leaf = nextLeaf(host, this.editable);\n                            if (!leaf) {\n                                return;\n                            }\n                            const leafEl = isBlock(leaf) ? leaf : leaf.parentElement;\n                            if (isBlock(leafEl) && leafEl.isContentEditable) {\n                                this.dependencies.selection.setSelection({\n                                    anchorNode: leafEl,\n                                    anchorOffset: 0,\n                                });\n                            }\n                        } catch {\n                            return;\n                        }\n                    },\n                },\n            });\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport {\n    HEADINGS,\n    TableOfContentManager,\n} from \"@html_editor/others/embedded_components/core/table_of_content/table_of_content_manager\";\n\nexport class TableOfContentPlugin extends Plugin {\n    static id = \"tableOfContent\";\n    static dependencies = [\"dom\", \"selection\", \"embeddedComponents\", \"link\", \"history\"];\n    resources = {\n        user_commands: [\n            {\n                id: \"insertTableOfContent\",\n                title: _t(\"Table Of Content\"),\n                description: _t(\"Highlight the structure (headings) of this field\"),\n                icon: \"fa-bookmark\",\n                run: this.insertTableOfContent.bind(this),\n            },\n        ],\n        powerbox_items: [\n            {\n                categoryId: \"navigation\",\n                commandId: \"insertTableOfContent\",\n            },\n        ],\n\n        /** Handlers */\n        restore_savepoint_handlers: () => this.delayedUpdateTableOfContents(this.editable),\n        history_reset_handlers: () => this.delayedUpdateTableOfContents(this.editable),\n        history_reset_from_steps_handlers: () => this.delayedUpdateTableOfContents(this.editable),\n        step_added_handlers: ({ stepCommonAncestor }) =>\n            this.delayedUpdateTableOfContents(stepCommonAncestor),\n        external_step_added_handlers: this.delayedUpdateTableOfContents.bind(this, this.editable),\n        clean_for_save_handlers: this.cleanForSave.bind(this),\n        mount_component_handlers: this.setupNewToc.bind(this),\n\n        system_classes: [\"o_embedded_toc_header_highlight\"],\n    };\n\n    setup() {\n        this.manager = new TableOfContentManager({\n            el: this.editable,\n        });\n        this.alive = true;\n    }\n\n    insertTableOfContent() {\n        const tableOfContentBlueprint = renderToElement(\"html_editor.TableOfContentBlueprint\");\n        this.dependencies.dom.insert(tableOfContentBlueprint);\n        this.dependencies.history.addStep();\n    }\n\n    /**\n     * @param {HTMLElement} root\n     */\n    cleanForSave({ root }) {\n        for (const el of root.querySelectorAll(\".o_embedded_toc_header_highlight\")) {\n            el.classList.remove(\"o_embedded_toc_header_highlight\");\n        }\n    }\n\n    destroy() {\n        super.destroy();\n        this.alive = false;\n    }\n\n    delayedUpdateTableOfContents(element) {\n        const selector = HEADINGS.join(\",\");\n        if (!(!element || element.querySelector(selector) || element.closest(selector))) {\n            return;\n        }\n        if (this.updateTimeout) {\n            window.clearTimeout(this.updateTimeout);\n        }\n        this.updateTimeout = window.setTimeout(() => {\n            if (!this.alive) {\n                return;\n            }\n            this.manager.updateStructure();\n        }, 500);\n    }\n\n    setupNewToc({ name, props }) {\n        if (name === \"tableOfContent\") {\n            Object.assign(props, {\n                manager: this.manager,\n            });\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { VideoSelectorDialog } from \"@html_editor/others/embedded_components/plugins/video_plugin/video_selector_dialog/video_selector_dialog\";\nimport { renderToElement } from \"@web/core/utils/render\";\n\nexport class VideoPlugin extends Plugin {\n    static id = \"video\";\n    static dependencies = [\"embeddedComponents\", \"dom\", \"selection\", \"link\", \"history\"];\n    resources = {\n        user_commands: [\n            {\n                id: \"openVideoSelectorDialog\",\n                title: _t(\"Video Link\"),\n                description: _t(\"Insert a Video\"),\n                icon: \"fa-play\",\n                run: () => {\n                    this.openVideoSelectorDialog((media) => {\n                        this.insertVideo(media);\n                    });\n                },\n            },\n        ],\n        powerbox_items: [\n            {\n                categoryId: \"navigation\",\n                commandId: \"openVideoSelectorDialog\",\n            },\n        ],\n    };\n\n    /**\n     * Inserts a video in the editor\n     * @param {Object} media\n     */\n    insertVideo(media) {\n        const videoBlock = renderToElement(\"html_editor.EmbeddedVideoBlueprint\", {\n            embeddedProps: JSON.stringify({\n                videoId: media.videoId,\n                platform: media.platform,\n                params: media.params || {},\n            }),\n        });\n        this.dependencies.dom.insert(videoBlock);\n        this.dependencies.history.addStep();\n    }\n\n    /**\n     * Inserts a dialog allowing the user to insert a video\n     * @param {function} save\n     */\n    openVideoSelectorDialog(save) {\n        const selection = this.dependencies.selection.getEditableSelection();\n        let restoreSelection = () => {\n            this.dependencies.selection.setSelection(selection);\n        };\n        this.services.dialog.add(\n            VideoSelectorDialog,\n            {\n                save: (media) => {\n                    save(media);\n                    restoreSelection = () => {};\n                },\n            },\n            {\n                onClose: () => {\n                    restoreSelection();\n                },\n            }\n        );\n    }\n}\n", "import { VideoSelector } from \"@html_editor/main/media/media_dialog/video_selector\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class VideoSelectorDialog extends Component {\n    static template = \"html_editor.VideoSelectorDialog\";\n    static components = { Dialog, VideoSelector };\n    static props = {\n        save: { type: Function },\n        close: { type: Function },\n    };\n\n    setup() {\n        super.setup();\n        this.media = {};\n        this.state = useState({\n            enableInsertVideoButton: false,\n        });\n    }\n\n    /**\n     * Callback function called whenever the video url provided by the user changes.\n     * When the video url is empty, the callback function will be called with an\n     * empty object ({}) to notify the parent component that the url changes.\n     * @param {Object} media\n     * @param {string} [media.id]\n     * @param {string} [media.src]\n     * @param {string} [media.platform]\n     * @param {Object} [media.params]\n     */\n    selectMedia(media) {\n        this.media = media;\n        this.state.enableInsertVideoButton = !!this.media.src;\n    }\n\n    /**\n     * @param {Event} event\n     */\n    onInsertVideoBtnClick(event) {\n        this.props.save(this.media);\n        this.props.close();\n    }\n}\n", "import { Component, useState } from \"@odoo/owl\";\n\nexport class QWebPicker extends Component {\n    static template = \"html_editor.QWebPicker\";\n    static props = [\"groups\", \"select\"];\n\n    setup() {\n        this.state = useState({ groups: this.props.groups });\n    }\n\n    onChange(ev) {\n        const [groupIndex, elementIndex] = ev.target.value.split(\",\");\n        this.props.select(this.state.groups[groupIndex][elementIndex].node);\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { closestElement, selectElements } from \"@html_editor/utils/dom_traversal\";\nimport { leftPos, rightPos } from \"@html_editor/utils/position\";\nimport { QWebPicker } from \"./qweb_picker\";\nimport { isElement } from \"@html_editor/utils/dom_info\";\n\nconst isUnsplittableQWebElement = (node) =>\n    isElement(node) &&\n    (node.tagName === \"T\" ||\n        [\n            \"t-field\",\n            \"t-if\",\n            \"t-elif\",\n            \"t-else\",\n            \"t-foreach\",\n            \"t-value\",\n            \"t-esc\",\n            \"t-out\",\n            \"t-raw\",\n        ].some((attr) => node.getAttribute(attr)));\n\nconst PROTECTED_QWEB_SELECTOR = \"[t-esc], [t-raw], [t-out], [t-field]\";\n\nexport class QWebPlugin extends Plugin {\n    static id = \"qweb\";\n    static dependencies = [\"overlay\", \"protectedNode\", \"selection\"];\n    resources = {\n        /** Handlers */\n        selectionchange_handlers: this.onSelectionChange.bind(this),\n        clean_handlers: this.clearDataAttributes.bind(this),\n        clean_for_save_handlers: ({ root }) => {\n            this.clearDataAttributes(root);\n            for (const element of root.querySelectorAll(PROTECTED_QWEB_SELECTOR)) {\n                element.removeAttribute(\"contenteditable\");\n                delete element.dataset.oeProtected;\n            }\n        },\n        normalize_handlers: this.normalize.bind(this),\n\n        savable_mutation_record_predicates: this.isMutationRecordSavable.bind(this),\n        unremovable_node_predicates: (node) =>\n            node.getAttribute?.(\"t-set\") || node.getAttribute?.(\"t-call\"),\n        unsplittable_node_predicates: isUnsplittableQWebElement,\n    };\n\n    setup() {\n        this.editable.classList.add(\"odoo-editor-qweb\");\n        this.picker = this.dependencies.overlay.createOverlay(QWebPicker, {\n            positionOptions: { position: \"top-start\" },\n        });\n        this.addDomListener(this.editable, \"click\", this.onClick);\n        this.groupIndex = 0;\n    }\n    isMutationRecordSavable(mutationRecord) {\n        if (mutationRecord.type === \"attributes\") {\n            if (\n                [\n                    \"data-oe-t-group\",\n                    \"data-oe-t-inline\",\n                    \"data-oe-t-selectable\",\n                    \"data-oe-t-group-active\",\n                ].includes(mutationRecord.attributeName)\n            ) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    isValidTargetForDomListener(ev) {\n        if (\n            ev.type === \"click\" &&\n            ev.target &&\n            closestElement(ev.target, PROTECTED_QWEB_SELECTOR)\n        ) {\n            // Allow clicking on a protected QWEB node to open the custom toolbar.\n            return true;\n        }\n        return super.isValidTargetForDomListener(ev);\n    }\n\n    /**\n     * @param { SelectionData } selectionData\n     */\n    onSelectionChange(selectionData) {\n        const selection = selectionData.documentSelection;\n        const qwebNode =\n            selection &&\n            selection.anchorNode &&\n            closestElement(selection.anchorNode, \"[t-field],[t-esc],[t-out]\");\n        if (qwebNode && this.editable.contains(qwebNode)) {\n            // select the whole qweb node\n            const [anchorNode, anchorOffset] = leftPos(qwebNode);\n            const [focusNode, focusOffset] = rightPos(qwebNode);\n            this.dependencies.selection.setSelection({\n                anchorNode,\n                anchorOffset,\n                focusNode,\n                focusOffset,\n            });\n        }\n    }\n\n    normalize(root) {\n        this.normalizeInline(root);\n\n        for (const element of selectElements(root, PROTECTED_QWEB_SELECTOR)) {\n            this.dependencies.protectedNode.setProtectingNode(element, true);\n        }\n        this.applyGroupQwebBranching(root);\n    }\n\n    checkAllInline(el) {\n        return [...el.children].every((child) => {\n            if (child.tagName === \"T\") {\n                return this.checkAllInline(child);\n            } else {\n                return (\n                    child.nodeType !== Node.ELEMENT_NODE ||\n                    this.document.defaultView.getComputedStyle(child).display === \"inline\"\n                );\n            }\n        });\n    }\n\n    normalizeInline(root) {\n        for (const el of selectElements(root, \"t\")) {\n            if (this.checkAllInline(el)) {\n                el.setAttribute(\"data-oe-t-inline\", \"true\");\n            }\n        }\n    }\n\n    getNodeGroups(node) {\n        const branchNode = node.closest(\"[data-oe-t-group]\");\n        if (!branchNode) {\n            return [];\n        }\n        const groupId = branchNode.getAttribute(\"data-oe-t-group\");\n        const group = [];\n        for (const node of branchNode.parentElement.querySelectorAll(\n            `[data-oe-t-group='${groupId}']`\n        )) {\n            let label = \"\";\n            if (node.hasAttribute(\"t-if\")) {\n                label = `if: ${node.getAttribute(\"t-if\")}`;\n            } else if (node.hasAttribute(\"t-elif\")) {\n                label = `elif: ${node.getAttribute(\"t-elif\")}`;\n            } else if (node.hasAttribute(\"t-else\")) {\n                label = \"else\";\n            }\n            group.push({\n                groupId,\n                node,\n                label,\n                isActive: node.getAttribute(\"data-oe-t-group-active\") === \"true\",\n            });\n        }\n        return this.getNodeGroups(branchNode.parentElement).concat([group]);\n    }\n\n    onClick(ev) {\n        this.picker.close();\n        const targetNode = ev.target;\n        if (targetNode.closest(\"[data-oe-t-group]\")) {\n            this.selectNode(targetNode);\n        }\n    }\n\n    selectNode(node) {\n        this.selectedNode = node;\n        this.picker.open({\n            target: node,\n            props: {\n                groups: this.getNodeGroups(node),\n                select: this.select.bind(this),\n            },\n        });\n    }\n\n    applyGroupQwebBranching(root) {\n        const tNodes = selectElements(root, \"[t-if], [t-elif], [t-else]\");\n        const groupsEncounter = new Set();\n        for (const node of tNodes) {\n            const prevNode = node.previousElementSibling;\n\n            let groupId;\n            if (prevNode && !node.hasAttribute(\"t-if\")) {\n                // Make the first t-if selectable, if prevNode is not a t-if,\n                // it's already data-oe-t-selectable.\n                prevNode.setAttribute(\"data-oe-t-selectable\", \"true\");\n                groupId = parseInt(prevNode.getAttribute(\"data-oe-t-group\"));\n                node.setAttribute(\"data-oe-t-selectable\", \"true\");\n            } else {\n                groupId = this.groupIndex++;\n            }\n            groupsEncounter.add(groupId);\n            node.setAttribute(\"data-oe-t-group\", groupId);\n        }\n        for (const groupId of groupsEncounter) {\n            const isOneElementActive = root.querySelector(\n                `[data-oe-t-group='${groupId}'][data-oe-t-group-active]`\n            );\n            // If there is no element in groupId activated, activate the first\n            // one.\n            if (!isOneElementActive) {\n                const firstElementToActivate = selectElements(\n                    root,\n                    `[data-oe-t-group='${groupId}']`\n                ).next().value;\n                firstElementToActivate.setAttribute(\"data-oe-t-group-active\", \"true\");\n            }\n        }\n    }\n\n    select(node) {\n        const groupId = node.getAttribute(\"data-oe-t-group\");\n        const activeElement = node.parentElement.querySelector(\n            `[data-oe-t-group='${groupId}'][data-oe-t-group-active]`\n        );\n        if (activeElement === node) {\n            return;\n        }\n        activeElement.removeAttribute(\"data-oe-t-group-active\");\n        node.setAttribute(\"data-oe-t-group-active\", \"true\");\n        this.selectedNode = node;\n        this.picker.close();\n        this.selectNode(node);\n    }\n\n    clearDataAttributes(root) {\n        for (const node of root.querySelectorAll(\n            \"[data-oe-t-group], [data-oe-t-inline], [data-oe-t-selectable], [data-oe-t-group-active]\"\n        )) {\n            node.removeAttribute(\"data-oe-t-group-active\");\n            node.removeAttribute(\"data-oe-t-group\");\n            node.removeAttribute(\"data-oe-t-inline\");\n            node.removeAttribute(\"data-oe-t-selectable\");\n        }\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { ImageField, imageField } from \"@web/views/fields/image/image_field\";\nimport { CustomMediaDialog } from \"./custom_media_dialog\";\nimport { getVideoUrl } from \"@html_editor/utils/url\";\n\nexport class X2ManyImageField extends ImageField {\n    static template = \"html_editor.ImageField\";\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.dialog = useService(\"dialog\");\n    }\n\n    /**\n     * New method and a new edit button is introduced here to overwrite,\n     * standard behavior of opening file input box in order to update a record.\n     */\n    onFileEdit(ev) {\n        const isVideo = this.props.record.data.video_url;\n        let mediaEl;\n        if (isVideo) {\n            mediaEl = document.createElement(\"img\");\n            mediaEl.dataset.src = this.props.record.data.video_url;\n        }\n        this.dialog.add(CustomMediaDialog, {\n            noIcons: true,\n            media: mediaEl,\n            activeTab: isVideo ? \"VIDEOS\" : \"IMAGES\",\n            save: (el) => {}, // Simple rebound to fake its execution\n            imageSave: this.onImageSave.bind(this),\n            videoSave: this.onVideoSave.bind(this),\n        });\n    }\n\n    async onImageSave(attachment) {\n        const attachmentRecord = await this.orm.searchRead(\n            \"ir.attachment\",\n            [[\"id\", \"=\", attachment[0].id]],\n            [\"id\", \"datas\", \"name\"],\n            {}\n        );\n        if (!attachmentRecord[0].datas) {\n            // URL type attachments are mostly demo records which don't have any ir.attachment datas\n            // TODO: make it work with URL type attachments\n            return this.notification.add(`Cannot add URL type attachment \"${attachmentRecord[0].name}\". Please try to reupload this image.`, {\n                type: \"warning\",\n            });\n        }\n        await this.props.record.update({\n            [this.props.name]: attachmentRecord[0].datas,\n            name: attachmentRecord[0].name,\n        });\n    }\n\n    async onVideoSave(videoInfo) {\n        const url = getVideoUrl(videoInfo[0].platform, videoInfo[0].videoId, videoInfo[0].params);\n        await this.props.record.update({\n            video_url: url.href,\n            name: videoInfo[0].platform + \" - [Video]\",\n        });\n    }\n\n    onFileRemove() {\n        const parentRecord = this.props.record._parentRecord.data;\n        parentRecord[this.env.parentField].delete(this.props.record);\n    }\n}\n\nexport const x2ManyImageField = {\n    ...imageField,\n    component: X2ManyImageField,\n};\n\nregistry.category(\"fields\").add(\"x2_many_image\", x2ManyImageField);\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { X2ManyField, x2ManyField } from \"@web/views/fields/x2many/x2many_field\";\nimport { getVideoUrl } from \"../utils/url\";\nimport { useChildSubEnv } from \"@odoo/owl\";\nimport { CustomMediaDialog } from \"./custom_media_dialog\";\n\nexport class X2ManyMediaViewer extends X2ManyField {\n    static template = \"html_editor.X2ManyMediaViewer\";\n    static props = {\n        ...X2ManyField.props,\n        convertToWebp: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        super.setup();\n        this.dialogs = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n        this.notification = useService(\"notification\");\n        this.supportedFields = [\"image_1920\", \"image_1024\", \"image_512\", \"image_256\", \"image_128\"];\n        useChildSubEnv({\n            parentField: this.props.name,\n        });\n    }\n\n    addMedia() {\n        this.dialogs.add(CustomMediaDialog, {\n            save: (el) => {}, // Simple rebound to fake its execution\n            multiImages: true,\n            noIcons: true,\n            imageSave: this.onImageSave.bind(this),\n            videoSave: this.onVideoSave.bind(this),\n        });\n    }\n\n    onVideoSave(videoInfo) {\n        const url = getVideoUrl(videoInfo[0].platform, videoInfo[0].videoId, videoInfo[0].params);\n        const videoList = this.props.record.data[this.props.name];\n        videoList.addNewRecord({ position: \"bottom\" }).then((record) => {\n            record.update({ name: videoInfo[0].platform + \" - [Video]\", video_url: url.href });\n        });\n    }\n\n    async onImageSave(attachments) {\n        const attachmentIds = attachments.map((attachment) => attachment.id);\n        const attachmentRecords = await this.orm.searchRead(\n            \"ir.attachment\",\n            [[\"id\", \"in\", attachmentIds]],\n            [\"id\", \"datas\", \"name\", \"mimetype\"],\n            {}\n        );\n        for (let attachment of attachmentRecords) {\n            const imageList = this.props.record.data[this.props.name];\n            if (!attachment.datas) {\n                // URL type attachments are mostly demo records which don't have any ir.attachment datas\n                // TODO: make it work with URL type attachments\n                return this.notification.add(`Cannot add URL type attachment \"${attachment.name}\". Please try to reupload this image.`, {\n                    type: \"warning\",\n                });\n            }\n            if (\n                this.props.convertToWebp &&\n                ![\"image/gif\", \"image/svg+xml\"].includes(attachment.mimetype)\n            ) {\n                // This method is widely adapted from onFileUploaded in ImageField.\n                // Upon change, make sure to verify whether the same change needs\n                // to be applied on both sides.\n                // Generate alternate sizes and format for reports.\n                const image = document.createElement(\"img\");\n                image.src = `data:${attachment.mimetype};base64,${attachment.datas}`;\n                await new Promise((resolve) => image.addEventListener(\"load\", resolve));\n\n                const originalSize = Math.max(image.width, image.height);\n                const smallerSizes = [1024, 512, 256, 128].filter((size) => size < originalSize);\n                let referenceId = undefined;\n\n                for (const size of [originalSize, ...smallerSizes]) {\n                    const ratio = size / originalSize;\n                    const canvas = document.createElement(\"canvas\");\n                    canvas.width = image.width * ratio;\n                    canvas.height = image.height * ratio;\n                    const ctx = canvas.getContext(\"2d\");\n                    ctx.drawImage(\n                        image, 0, 0, image.width, image.height, 0, 0, canvas.width, canvas.height\n                    );\n\n                    // WebP format\n                    const webpData = canvas.toDataURL(\"image/webp\", 0.75).split(\",\")[1];\n                    const [resizedId] = await this.orm.call(\"ir.attachment\", \"create_unique\", [\n                        [\n                            {\n                                name: attachment.name.replace(/\\.[^/.]+$/, \".webp\"),\n                                description:\n                                size === originalSize ? \"\" : `resize: ${size}`,\n                                datas: webpData,\n                                res_id: referenceId,\n                                res_model: \"ir.attachment\",\n                                mimetype: \"image/webp\",\n                            },\n                        ],\n                    ]);\n\n                    referenceId = referenceId || resizedId;\n\n                    // JPEG format for compatibility\n                    const jpegData = canvas.toDataURL(\"image/jpeg\", 0.75).split(\",\")[1];\n                    await this.orm.call(\"ir.attachment\", \"create_unique\", [\n                        [\n                            {\n                                name: attachment.name.replace(/\\.[^/.]+$/, \".jpg\"),\n                                description: `resize: ${size} - format: jpeg`,\n                                datas: jpegData,\n                                res_id: resizedId,\n                                res_model: \"ir.attachment\",\n                                mimetype: \"image/jpeg\",\n                            }\n                        ],\n                    ]);\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.drawImage(image, 0, 0, image.width, image.height);\n\n                const webpData = canvas.toDataURL(\"image/webp\", 0.75).split(\",\")[1];\n                attachment.datas = webpData;\n                attachment.mimetype = \"image/webp\";\n                attachment.name = attachment.name.replace(/\\.[^/.]+$/, \".webp\");\n            }\n\n            imageList.addNewRecord({ position: \"bottom\" }).then((record) => {\n                const activeFields = imageList.activeFields;\n                const updateData = {};\n                for (const field in activeFields) {\n                    if (attachment.datas && this.supportedFields.includes(field)) {\n                        updateData[field] = attachment.datas;\n                        updateData[\"name\"] = attachment.name;\n                    }\n                }\n                record.update(updateData);\n            });\n        }\n    }\n\n    async onAdd({ context, editable } = {}) {\n        this.addMedia();\n    }\n}\n\nexport const x2ManyMediaViewer = {\n    ...x2ManyField,\n    component: X2ManyMediaViewer,\n    extractProps: (\n        { attrs, relatedFields, viewMode, views, widget, options, string },\n        dynamicInfo\n    ) => {\n        const x2ManyFieldProps = x2ManyField.extractProps(\n            { attrs, relatedFields, viewMode, views, widget, options, string },\n            dynamicInfo\n        );\n        return {\n            ...x2ManyFieldProps,\n            convertToWebp: options.convert_to_webp,\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"x2_many_media_viewer\", x2ManyMediaViewer);\n", "import { isProtected, isProtecting, isUnprotecting } from \"./utils/dom_info\";\n\n/**\n * @typedef { import(\"./editor\").Editor } Editor\n * @typedef { import(\"./plugin_sets\").SharedMethods } SharedMethods\n */\n\nexport class Plugin {\n    static id = \"\";\n    static dependencies = [];\n    static shared = [];\n\n    /**\n     * @param {Editor['document']} document\n     * @param {Editor['editable']} editable\n     * @param {SharedMethods} dependencies\n     * @param {import(\"./editor\").EditorConfig} config\n     * @param {*} services\n     */\n    constructor(document, editable, dependencies, config, services) {\n        /** @type { Document } **/\n        this.document = document;\n        /** @type { HTMLElement } **/\n        this.editable = editable;\n        /** @type { EditorConfig } **/\n        this.config = config;\n        this.services = services;\n        /** @type { SharedMethods } **/\n        this.dependencies = dependencies;\n        this._cleanups = [];\n        /**\n         * The resources aggregated from all the plugins by the editor.\n         */\n        this._resources = null; // set before start\n        this.isDestroyed = false;\n    }\n\n    setup() {}\n\n    isValidTargetForDomListener(ev) {\n        return !isProtecting(ev.target) && (!isProtected(ev.target) || isUnprotecting(ev.target));\n    }\n\n    addDomListener(target, eventName, fn, capture = false) {\n        const handler = (ev) => {\n            if (this.isValidTargetForDomListener(ev)) {\n                fn?.call(this, ev);\n            }\n        };\n        target.addEventListener(eventName, handler, capture);\n        this._cleanups.push(() => target.removeEventListener(eventName, handler, capture));\n    }\n\n    /**\n     * @param {string} resourceId\n     * @returns {Array}\n     */\n    getResource(resourceId) {\n        return this._resources[resourceId] || [];\n    }\n\n    /**\n     * Execute the functions registered under resourceId with the given\n     * arguments.\n     *\n     * This function is meant to enhance code readability by clearly expressing\n     * its intent.\n     *\n     * This function can be thought as an event dispatcher, calling the handlers\n     * with `args` as the payload.\n     *\n     * Example:\n     * ```js\n     * this.dispatchTo(\"my_event_handlers\", arg1, arg2);\n     * ```\n     *\n     * @param {string} resourceId\n     * @param  {...any} args The arguments to pass to the handlers.\n     */\n    dispatchTo(resourceId, ...args) {\n        this.getResource(resourceId).forEach((handler) => handler(...args));\n    }\n\n    /**\n     * Execute a series of functions until one of them returns a truthy value.\n     *\n     * This function is meant to enhance code readability by clearly expressing\n     * its intent.\n     *\n     * A command \"delegates\" its execution to one of the overriding functions,\n     * which return a truthy value to signal it has been handled.\n     *\n     * It is the the caller's responsability to stop the execution when this\n     * function returns true.\n     *\n     * Example:\n     * ```js\n     * if (this.delegateTo(\"my_command_overrides\", arg1, arg2)) {\n     *   return;\n     * }\n     * ```\n     *\n     * @param {string} resourceId\n     * @param  {...any} args The arguments to pass to the overrides.\n     * @returns {boolean} Whether one of the overrides returned a truthy value.\n     */\n    delegateTo(resourceId, ...args) {\n        return this.getResource(resourceId).some((fn) => fn(...args));\n    }\n\n    destroy() {\n        for (const cleanup of this._cleanups) {\n            cleanup();\n        }\n        this.isDestroyed = true;\n    }\n}\n", "import { BaseContainerPlugin } from \"./core/base_container_plugin\";\nimport { ClipboardPlugin } from \"./core/clipboard_plugin\";\nimport { CommentPlugin } from \"./core/comment_plugin\";\nimport { DeletePlugin } from \"./core/delete_plugin\";\nimport { DialogPlugin } from \"./core/dialog_plugin\";\nimport { DomPlugin } from \"./core/dom_plugin\";\nimport { FormatPlugin } from \"./core/format_plugin\";\nimport { HistoryPlugin } from \"./core/history_plugin\";\nimport { InputPlugin } from \"./core/input_plugin\";\nimport { LineBreakPlugin } from \"./core/line_break_plugin\";\nimport { NoInlineRootPlugin } from \"./core/no_inline_root_plugin\";\nimport { OverlayPlugin } from \"./core/overlay_plugin\";\nimport { ProtectedNodePlugin } from \"./core/protected_node_plugin\";\nimport { SanitizePlugin } from \"./core/sanitize_plugin\";\nimport { SelectionPlugin } from \"./core/selection_plugin\";\nimport { ShortCutPlugin } from \"./core/shortcut_plugin\";\nimport { SplitPlugin } from \"./core/split_plugin\";\nimport { UserCommandPlugin } from \"./core/user_command_plugin\";\nimport { AlignPlugin } from \"./main/align_plugin\";\nimport { BannerPlugin } from \"./main/banner_plugin\";\nimport { ChatGPTPlugin } from \"./main/chatgpt/chatgpt_plugin\";\nimport { ColumnPlugin } from \"./main/column_plugin\";\nimport { EmojiPlugin } from \"./main/emoji_plugin\";\nimport { ColorPlugin } from \"./main/font/color_plugin\";\nimport { FeffPlugin } from \"./main/feff_plugin\";\nimport { FontPlugin } from \"./main/font/font_plugin\";\nimport { HintPlugin } from \"./main/hint_plugin\";\nimport { InlineCodePlugin } from \"./main/inline_code\";\nimport { LinkPastePlugin } from \"./main/link/link_paste_plugin\";\nimport { LinkPlugin } from \"./main/link/link_plugin\";\nimport { OdooLinkSelectionPlugin } from \"./main/link/link_selection_odoo_plugin\";\nimport { LinkSelectionPlugin } from \"./main/link/link_selection_plugin\";\nimport { ListPlugin } from \"./main/list/list_plugin\";\nimport { LocalOverlayPlugin } from \"./main/local_overlay_plugin\";\nimport { FilePlugin } from \"./main/media/file_plugin\";\nimport { IconPlugin } from \"./main/media/icon_plugin\";\nimport { ImageCropPlugin } from \"./main/media/image_crop_plugin\";\nimport { ImagePlugin } from \"./main/media/image_plugin\";\nimport { MediaPlugin } from \"./main/media/media_plugin\";\nimport { MoveNodePlugin } from \"./main/movenode_plugin\";\nimport { PowerButtonsPlugin } from \"./main/power_buttons_plugin\";\nimport { PositionPlugin } from \"./main/position_plugin\";\nimport { PowerboxPlugin } from \"./main/powerbox/powerbox_plugin\";\nimport { SearchPowerboxPlugin } from \"./main/powerbox/search_powerbox_plugin\";\nimport { SignaturePlugin } from \"./main/signature_plugin\";\nimport { StarPlugin } from \"./main/star_plugin\";\nimport { TablePlugin } from \"./main/table/table_plugin\";\nimport { TableResizePlugin } from \"./main/table/table_resize_plugin\";\nimport { TableUIPlugin } from \"./main/table/table_ui_plugin\";\nimport { TabulationPlugin } from \"./main/tabulation_plugin\";\nimport { TextDirectionPlugin } from \"./main/text_direction_plugin\";\nimport { ToolbarPlugin } from \"./main/toolbar/toolbar_plugin\";\nimport { YoutubePlugin } from \"./main/youtube_plugin\";\nimport { CollaborationOdooPlugin } from \"./others/collaboration/collaboration_odoo_plugin\";\nimport { CollaborationPlugin } from \"./others/collaboration/collaboration_plugin\";\nimport { CollaborationSelectionAvatarPlugin } from \"./others/collaboration/collaboration_selection_avatar_plugin\";\nimport { CollaborationSelectionPlugin } from \"./others/collaboration/collaboration_selection_plugin\";\nimport { DynamicPlaceholderPlugin } from \"./others/dynamic_placeholder_plugin\";\nimport { EmbeddedComponentPlugin } from \"./others/embedded_component_plugin\";\nimport { TableOfContentPlugin } from \"@html_editor/others/embedded_components/plugins/table_of_content_plugin/table_of_content_plugin\";\nimport { VideoPlugin } from \"@html_editor/others/embedded_components/plugins/video_plugin/video_plugin\";\nimport { QWebPlugin } from \"./others/qweb_plugin\";\nimport { EditorVersionPlugin } from \"./core/editor_version_plugin\";\n\n/**\n * @typedef { Object } SharedMethods\n *\n * Core\n * @property { import(\"./core/clipboard_plugin\").ClipboardShared } clipboard\n * @property { import(\"./core/delete_plugin\").DeleteShared } delete\n * @property { import(\"./core/dialog_plugin\").DialogShared } dialog\n * @property { import(\"./core/dom_plugin\").DomShared } dom\n * @property { import(\"./core/format_plugin\").FormatShared } format\n * @property { import(\"./core/history_plugin\").HistoryShared } history\n * @property { import(\"./core/line_break_plugin\").LineBreakShared } lineBreak\n * @property { import(\"./core/overlay_plugin\").OverlayShared } overlay\n * @property { import(\"./core/protected_node_plugin\").ProtectedNodeShared } protectedNode\n * @property { import(\"./core/sanitize_plugin\").SanitizeShared } sanitize\n * @property { import(\"./core/selection_plugin\").SelectionShared } selection\n * @property { import(\"./core/split_plugin\").SplitShared } split\n * @property { import(\"./core/user_command_plugin\").UserCommandShared } userCommand\n *\n * Main\n * @property { import(\"./main/font/color_plugin\").ColorShared } color\n * @property { import(\"./main/link/link_plugin\").LinkShared } link\n * @property { import (\"./main/link/link_selection_plugin\").LinkSelectionShared } linkSelection\n * @property { import (\"./main/media/media_plugin\").MediaShared } media\n * @property { import(\"./main/powerbox/powerbox_plugin\").PowerboxShared } powerbox\n * @property { import (\"./main/table/table_plugin\").TableShared } table\n * @property { import (\"./main/toolbar/toolbar_plugin\").ToolbarShared } toolbar\n * @property { import (\"./main/emoji_plugin\").EmojiShared } emoji\n * @property { import (\"./main/local_overlay_plugin\").LocalOverlayShared } localOverlay\n * @property { import (\"./main/tabulation_plugin\").TabulationShared } tabulation\n * @property { import (\"./main/feff_plugin\").FeffShared } feff\n *\n * Others\n * @property { import(\"./others/collaboration/collaboration_odoo_plugin\").CollaborationOdooShared } collaborationOdoo\n * @property { import(\"./others/collaboration/collaboration_plugin\").CollaborationShared } collaboration\n * @property { import(\"./others/dynamic_placeholder_plugin\").DynamicPlaceholderShared } dynamicPlaceholder\n */\n\nexport const CORE_PLUGINS = [\n    BaseContainerPlugin,\n    ClipboardPlugin,\n    CommentPlugin,\n    DeletePlugin,\n    DialogPlugin,\n    DomPlugin,\n    EditorVersionPlugin,\n    FormatPlugin,\n    HistoryPlugin,\n    InputPlugin,\n    LineBreakPlugin,\n    NoInlineRootPlugin,\n    OverlayPlugin,\n    ProtectedNodePlugin,\n    SanitizePlugin,\n    SelectionPlugin,\n    SplitPlugin,\n    UserCommandPlugin,\n];\n\nexport const MAIN_PLUGINS = [\n    ...CORE_PLUGINS,\n    BannerPlugin,\n    ChatGPTPlugin,\n    ColorPlugin,\n    ColumnPlugin,\n    EmojiPlugin,\n    HintPlugin,\n    AlignPlugin,\n    ListPlugin,\n    MediaPlugin,\n    ShortCutPlugin,\n    PowerboxPlugin,\n    SearchPowerboxPlugin,\n    SignaturePlugin,\n    StarPlugin,\n    TablePlugin,\n    TableUIPlugin,\n    TabulationPlugin,\n    ToolbarPlugin,\n    FontPlugin, // note: if before ListPlugin, there are a few split tests that fails\n    YoutubePlugin,\n    IconPlugin,\n    ImagePlugin,\n    ImageCropPlugin,\n    LinkPlugin,\n    LinkPastePlugin,\n    FeffPlugin,\n    LinkSelectionPlugin,\n    OdooLinkSelectionPlugin,\n    PowerButtonsPlugin,\n    MoveNodePlugin,\n    LocalOverlayPlugin,\n    PositionPlugin,\n    TextDirectionPlugin,\n    InlineCodePlugin,\n    TableResizePlugin,\n    FilePlugin,\n];\n\nexport const COLLABORATION_PLUGINS = [\n    CollaborationPlugin,\n    CollaborationOdooPlugin,\n    CollaborationSelectionPlugin,\n    CollaborationSelectionAvatarPlugin,\n];\n\nexport const EMBEDDED_COMPONENT_PLUGINS = [\n    EmbeddedComponentPlugin,\n    TableOfContentPlugin,\n    VideoPlugin,\n];\n\nexport const DYNAMIC_PLACEHOLDER_PLUGINS = [DynamicPlaceholderPlugin, QWebPlugin];\n\nexport const EXTRA_PLUGINS = [\n    ...COLLABORATION_PLUGINS,\n    ...MAIN_PLUGINS,\n    ...EMBEDDED_COMPONENT_PLUGINS,\n    QWebPlugin,\n];\n", "import { ancestors } from \"@html_editor/utils/dom_traversal\";\nimport { throttleForAnimation } from \"@web/core/utils/timing\";\nimport { couldBeScrollableX, couldBeScrollableY } from \"@web/core/utils/scrolling\";\nimport { useComponent, useEffect } from \"@odoo/owl\";\n\n/**\n * This hook has the same job as the PositionPlugin, but for Components.\n * It was created to be used within the Html Viewer and still have overlays.\n *\n * TODO ABD: refactor html viewer to: either use a plugin system, or generalize\n * the positioning logic so that both the plugin and the hook can use it.\n */\nexport function usePositionHook(containerRef, document, callback) {\n    const comp = useComponent();\n    const onLayoutGeometryChange = throttleForAnimation(callback.bind(comp));\n    const resizeObserver = new ResizeObserver(onLayoutGeometryChange);\n    const cleanups = [];\n    const addDomListener = (target, eventName, capture) => {\n        target.addEventListener(eventName, onLayoutGeometryChange, capture);\n        cleanups.push(() => target.removeEventListener(eventName, onLayoutGeometryChange, capture));\n    };\n    useEffect(\n        () => {\n            if (containerRef.el) {\n                resizeObserver.observe(document.body);\n                resizeObserver.observe(containerRef.el);\n                addDomListener(window, \"resize\");\n                if (document.defaultView !== window) {\n                    addDomListener(document.defaultView, \"resize\");\n                }\n                const scrollableElements = [containerRef.el, ...ancestors(containerRef.el)].filter(\n                    (node) => {\n                        return couldBeScrollableX(node) || couldBeScrollableY(node);\n                    }\n                );\n                for (const scrollableElement of scrollableElements) {\n                    addDomListener(scrollableElement, \"scroll\");\n                    resizeObserver.observe(scrollableElement);\n                }\n            }\n            return () => {\n                resizeObserver.disconnect();\n                for (const cleanup of cleanups.toReversed()) {\n                    cleanup();\n                    cleanups.pop();\n                }\n            };\n        },\n        () => [containerRef.el]\n    );\n}\n", "import { registry } from \"@web/core/registry\";\n\nexport const uploadLocalFileService = {\n    dependencies: [\"upload\", \"orm\"],\n    start(env, { upload: uploadService, orm }) {\n        const input = document.createElement(\"input\");\n        input.type = \"file\";\n\n        /**\n         * Open the system file selector and return the selected files.\n         *\n         * @param {Object} [options]\n         * @param {boolean} [options.multiple=true]\n         * @param {string} [options.accept]\n         * @returns {Promise<FileList>}\n         */\n        async function selectLocalFiles({ multiple, accept }) {\n            input.multiple = multiple;\n            input.accept = accept;\n\n            // Open system's file selector\n            input.click();\n\n            // Wait for user to select files or cancel.\n            await new Promise((resolve) => {\n                const resolveAndClear = () => {\n                    resolve();\n                    input.removeEventListener(\"change\", resolveAndClear);\n                    input.removeEventListener(\"cancel\", resolveAndClear);\n                };\n                // Detect file(s) selected\n                input.addEventListener(\"change\", resolveAndClear);\n                // Detect file selector closed without selecting files (cancel)\n                input.addEventListener(\"cancel\", resolveAndClear);\n            });\n            return input.files;\n        }\n\n        /**\n         * @param {FileList} files\n         * @param {Object} recordInfo\n         * @returns {Promise<Object[]>} attachments\n         */\n        async function filesToAttachments(files, { resModel, resId }) {\n            const attachments = [];\n            await uploadService.uploadFiles(files, { resModel, resId }, (attachment) => {\n                attachments.push(attachment);\n            });\n            return attachments;\n        }\n\n        /**\n         * Open the system file selector and upload the selected files.\n         *\n         * @param {Object} recordInfo\n         * @param {Object} [options]\n         * @param {string} [options.accept] Accepted file types\n         * @param {boolean} [options.multiple=false] Allow multiple files to be selected\n         * @param {boolean} [options.accessToken=false] Add access token to uploaded files\n         * @returns {Promise<Object[]>} attachments\n         */\n        async function upload(\n            { resId, resModel },\n            { accept = \"*/*\", multiple = false, accessToken = false } = {}\n        ) {\n            try {\n                const files = await selectLocalFiles({ multiple, accept });\n                const attachments = await filesToAttachments(files, { resModel, resId });\n                if (accessToken && attachments.length && !attachments[0].public) {\n                    await addAccessToken(attachments);\n                }\n                return attachments;\n            } catch {\n                // The upload service displays a either a notification or an\n                // error message in the progress toast.\n                return [];\n            }\n        }\n\n        /**\n         * @param {Object[]} attachments\n         * @returns {Promise<Object[]>}\n         */\n        async function addAccessToken(attachments) {\n            const accessTokens = await orm.call(\"ir.attachment\", \"generate_access_token\", [\n                attachments.map((a) => a.id),\n            ]);\n            attachments.forEach((attachment, index) => {\n                attachment.access_token = accessTokens[index];\n            });\n            return attachments;\n        }\n\n        /**\n         * @param {Object} attachments\n         * @param {Object} [options]\n         * @returns {string}\n         */\n        function getURL(attachment, { unique, download, accessToken } = {}) {\n            let url = `/web/content/${attachment.id}`;\n            const queryParams = [];\n            if (unique) {\n                queryParams.push(`unique=${encodeURIComponent(attachment.checksum)}`);\n            }\n            if (download) {\n                queryParams.push(\"download=true\");\n            }\n            if (accessToken && attachment.access_token) {\n                queryParams.push(`access_token=${attachment.access_token}`);\n            }\n            if (queryParams.length) {\n                url += `?${queryParams.join(\"&\")}`;\n            }\n            return url;\n        }\n\n        return { upload, addAccessToken, getURL };\n    },\n};\n\nregistry.category(\"services\").add(\"uploadLocalFiles\", uploadLocalFileService);\n", "export const BASE_CONTAINER_CLASS = \"o-paragraph\";\n\nexport const SUPPORTED_BASE_CONTAINER_NAMES = [\"P\", \"DIV\"];\n\n/**\n * @param {string} [nodeName] @see SUPPORTED_BASE_CONTAINER_NAMES\n *                 will return the global selector if nodeName is not specified.\n * @returns {string} selector for baseContainers.\n */\nexport function getBaseContainerSelector(nodeName) {\n    if (!nodeName) {\n        return baseContainerGlobalSelector;\n    }\n    nodeName = SUPPORTED_BASE_CONTAINER_NAMES.includes(nodeName) ? nodeName : \"P\";\n    let suffix = \"\";\n    if (nodeName !== \"P\") {\n        suffix = `.${BASE_CONTAINER_CLASS}`;\n    }\n    return `${nodeName}${suffix}`;\n}\n\nexport const baseContainerGlobalSelector = SUPPORTED_BASE_CONTAINER_NAMES.map((name) =>\n    getBaseContainerSelector(name)\n).join(\",\");\n\n/**\n * Create a new baseContainer element.\n *\n * @param {string} nodeName @see SUPPORTED_BASE_CONTAINER_NAMES\n * @param {Document} [document] Used to create new baseContainer elements.\n *                   For iframes, preferably use the iframe document.\n *                   Fallbacks to the window document if possible and unspecified.\n *                   Has to be specified otherwise.\n * @returns {HTMLElement}\n */\nexport function createBaseContainer(nodeName, document) {\n    if (!document && window) {\n        document = window.document;\n    }\n    nodeName = nodeName && SUPPORTED_BASE_CONTAINER_NAMES.includes(nodeName) ? nodeName : \"P\";\n    const el = document.createElement(nodeName);\n    if (nodeName !== \"P\") {\n        el.className = BASE_CONTAINER_CLASS;\n    }\n    return el;\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\nexport const RGBA_REGEX = /[\\d.]{1,5}/g;\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(RGBA_REGEX) || [];\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(RGBA_REGEX) || []).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 { isParagraphRelatedElement, isShrunkBlock, isVisible } from \"./dom_info\";\nimport { callbacksForCursorUpdate } from \"./selection\";\nimport { isEmptyBlock, isPhrasingContent } from \"../utils/dom_info\";\nimport { childNodes } from \"./dom_traversal\";\nimport { childNodeIndex, DIRECTIONS } from \"./position\";\nimport {\n    baseContainerGlobalSelector,\n    createBaseContainer,\n} from \"@html_editor/utils/base_container\";\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 && isParagraphRelatedElement(child)) {\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(\n    element,\n    { baseContainerNodeName = \"P\", cursors = { update: () => {} } } = {}\n) {\n    // Helpers to manipulate preserving selection.\n    const wrapInBlock = (node, cursors) => {\n        const block = isPhrasingContent(node)\n            ? createBaseContainer(baseContainerNodeName, node.ownerDocument)\n            : node.ownerDocument.createElement(\"DIV\");\n        cursors.update(callbacksForCursorUpdate.append(block, node));\n        cursors.update(callbacksForCursorUpdate.before(node, block));\n        if (node.nextSibling) {\n            const sibling = node.nextSibling;\n            node.remove();\n            sibling.before(block);\n        } else {\n            const parent = node.parentElement;\n            node.remove();\n            parent.append(block);\n        }\n        block.append(node);\n        return block;\n    };\n    const appendToCurrentBlock = (currentBlock, node, cursors) => {\n        if (currentBlock.matches(baseContainerGlobalSelector) && !isPhrasingContent(node)) {\n            const block = currentBlock.ownerDocument.createElement(\"DIV\");\n            cursors.update(callbacksForCursorUpdate.before(currentBlock, block));\n            currentBlock.before(block);\n            for (const child of childNodes(currentBlock)) {\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    const children = childNodes(element);\n    const visibleNodes = new Set(children.filter(isVisible));\n\n    let currentBlock;\n    let shouldBreakLine = true;\n    for (const node of children) {\n        if (isBlock(node)) {\n            shouldBreakLine = true;\n        } else if (!visibleNodes.has(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.\n// TODO: deprecated, use the DomPlugin shared function 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    const classNamesSet = new Set(classNames);\n    if ([...element.classList].every((className) => classNamesSet.has(className))) {\n        element.removeAttribute(\"class\");\n    } else {\n        element.classList.remove(...classNames);\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 * @param {Array} predicates exceptions where a trailing BR should not be removed\n * @returns {HTMLElement|undefined} the removed br, if any\n */\nexport function cleanTrailingBR(el, predicates = []) {\n    const candidate = el?.lastChild;\n    if (\n        candidate?.nodeName === \"BR\" &&\n        candidate.previousSibling?.nodeName !== \"BR\" &&\n        !isEmptyBlock(el) &&\n        !predicates.some((predicate) => predicate(candidate))\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 * In web_editor the text nodes used to be replaced by new ones with the updated\n * text rather than just changing the text content of the node because it\n * creates different mutations and it used to break the tour system. In\n * html_editor the text content is changed instead because other plugins rely on\n * the reference to the text node.\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\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 {Text} textNode\n * @param {number} offset\n * @param {boolean} 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    const document = textNode.ownerDocument;\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", "import { baseContainerGlobalSelector } from \"./base_container\";\nimport { closestBlock, isBlock } from \"./blocks\";\nimport { childNodes, 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\nexport function isEmptyTextNode(node) {\n    return node.nodeType === Node.TEXT_NODE && node.nodeValue.length === 0;\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(node) : []).some((n) => isVisible(n));\n}\n\nexport function isButton(node) {\n    if (!node || node.nodeType !== Node.ELEMENT_NODE) {\n        return false;\n    }\n    return node.nodeName === \"BUTTON\" || node.classList.contains(\"btn\");\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(node) : []).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\nexport function containsAnyInline(element) {\n    if (!element) {\n        return false;\n    }\n    let child = element.firstChild;\n    while (child) {\n        if (\n            (!isBlock(child) && child.nodeType === Node.ELEMENT_NODE) ||\n            (child.nodeType === Node.TEXT_NODE && child.textContent.trim() !== \"\")\n        ) {\n            return true;\n        }\n        child = child.nextSibling;\n    }\n    return false;\n}\n\nexport function containsAnyNonPhrasingContent(element) {\n    if (!element) {\n        return false;\n    }\n    let child = element.firstChild;\n    while (child) {\n        if (!isPhrasingContent(child)) {\n            return true;\n        }\n        child = child.nextSibling;\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. It is non-exhaustive and should not be used as a\n// standalone. @see isParagraphRelatedElement\n// TODO add: this list should contain PRE, but the spec currently is to\n// paste flow content inside the PRE, so it is removed temporarily.\nexport const paragraphRelatedElements = [\"P\", \"H1\", \"H2\", \"H3\", \"H4\", \"H5\", \"H6\"];\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) && !isParagraphRelatedElement(node);\n}\n\nexport const phrasingContent = new Set([\"#text\", ...phrasingTagNames]);\nconst flowContent = new Set([...phrasingContent, ...paragraphRelatedElements, \"DIV\", \"HR\"]);\nexport const listItem = new Set([\"LI\"]);\nconst listContainers = new Set([\"UL\", \"OL\"]);\n\nconst allowedContent = {\n    BLOCKQUOTE: flowContent,\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: flowContent, // HTML spec: phrasing content\n    TD: flowContent,\n    TR: new Set([\"TD\"]),\n};\n\nexport function isParagraphRelatedElement(node) {\n    if (!node) {\n        return false;\n    }\n    return (\n        paragraphRelatedElements.includes(node.nodeName) ||\n        (node.nodeType === Node.ELEMENT_NODE && node.matches(baseContainerGlobalSelector))\n    );\n}\n\nexport const paragraphRelatedElementsSelector = [\n    ...paragraphRelatedElements,\n    baseContainerGlobalSelector,\n].join(\",\");\n\nexport function isListItemElement(node) {\n    return [...listItem].includes(node.nodeName);\n}\n\nexport const listItemElementSelector = [...listItem].join(\",\");\n\nexport function isListElement(node) {\n    return [...listContainers].includes(node.nodeName);\n}\n\nexport const listElementSelector = [...listContainers].join(\",\");\n\n/**\n * @param {Element} parentBlock\n * @param {Node[]} nodes\n * @returns {boolean}\n */\nexport function isAllowedContent(parentBlock, nodes) {\n    let 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    if (parentBlock.matches(baseContainerGlobalSelector)) {\n        // A baseContainer DIV can only have phrasingContent, as a P would.\n        allowedContentSet = phrasingContent;\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            const childrenNodes = childNodes(node);\n            direction = offset < childrenNodes.length;\n            next = childrenNodes[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 isElement(node) {\n    return node.nodeType === Node.ELEMENT_NODE;\n}\n\nexport function isContentEditable(node) {\n    const element = isTextNode(node) ? node.parentElement : node;\n    return element && element.isContentEditable;\n}\n\nexport function isContentEditableAncestor(node) {\n    if (node.nodeType !== Node.ELEMENT_NODE) {\n        return false;\n    }\n    return node.isContentEditable && node.matches(\"[contenteditable]\");\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\n/**\n * Call this function to start watching for mutations.\n * Call the returned function to stop watching and get the mutation records.\n *\n * @returns {() => MutationRecord[]}\n */\nexport function observeMutations(target, observerOptions) {\n    const records = [];\n    const observerCallback = (mutations) => records.push(...mutations);\n    const observer = new MutationObserver(observerCallback);\n    observer.observe(target, observerOptions);\n    return () => {\n        observerCallback(observer.takeRecords());\n        observer.disconnect();\n        return records;\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    \"o_small-fs\",\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) => closestElement(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) =>\n            FONT_SIZE_CLASSES.find((cls) => closestElement(node)?.classList?.contains(cls)),\n        hasStyle: (node, props) => FONT_SIZE_CLASSES.find((cls) => node.classList.contains(cls)),\n        addStyle: (node, props) => {\n            node.style.removeProperty(\"font-size\");\n            node.classList.add(props.className);\n        },\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 = [\n        \"display-1\",\n        \"display-2\",\n        \"display-3\",\n        \"display-4\",\n        \"lead\",\n    ];\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    const pxValue = remValue && 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\n// Deprecated, use the ListPlugin shared function instead.\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 * Deprecated, use the ListPlugin shared function instead.\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 * @param {Object} options\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 * Deprecated, use the ListPlugin shared function instead.\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 * @param {Object} options\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    if (isTextNode) {\n        return node.length;\n    } else {\n        const child = node.lastChild;\n        return child ? childNodeIndex(child) + 1 : 0;\n    }\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 { containsAnyInline } from \"./dom_info\";\nimport { wrapInlinesInBlocks } from \"./dom\";\nimport { markup } from \"@odoo/owl\";\n\nexport function initElementForEdition(element, options = {}) {\n    if (\n        element?.nodeType === Node.ELEMENT_NODE &&\n        containsAnyInline(element) &&\n        !options.allowInlineAtRoot\n    ) {\n        // No matter the inline content, it will be wrapped in a DIV to try\n        // and match the current style of the content as much as possible.\n        // (P has a margin-bottom, DIV does not).\n        wrapInlinesInBlocks(element, {\n            baseContainerNodeName: \"DIV\",\n        });\n    }\n}\n\n/**\n * Properly close common XML-like self-closing elements to avoid HTML parsing\n * issues.\n *\n * @param {string} content\n * @returns {string}\n */\nexport function fixInvalidHTML(content) {\n    if (!content) {\n        return content;\n    }\n    // TODO: improve the regex to support nodes with data-attributes containing\n    // `/` and `>` characters.\n    const regex = /<\\s*(a|strong|t)[^<]*?\\/\\s*>/g;\n    return content.replace(regex, (match, g0) => match.replace(/\\/\\s*>/, `></${g0}>`));\n}\n\nlet Markup = null;\n\nexport function instanceofMarkup(value) {\n    if (!Markup) {\n        Markup = markup(\"\").constructor;\n    }\n    return value instanceof Markup;\n}\n", "import { closestBlock, isBlock } from \"./blocks\";\nimport {\n    getDeepestPosition,\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    [focusNode, focusOffset] = getDeepestPosition(focusNode, focusOffset);\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    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", "/**\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 { Component, onMounted, onWillDestroy, useRef, useState, useSubEnv } from \"@odoo/owl\";\nimport { Editor } from \"./editor\";\nimport { Toolbar } from \"./main/toolbar/toolbar\";\nimport { useChildRef, useSpellCheck } from \"@web/core/utils/hooks\";\nimport { LocalOverlayContainer } from \"./local_overlay_container\";\nimport { uniqueId } from \"@web/core/utils/functions\";\n\n/**\n * @typedef { import(\"./editor\").EditorConfig } EditorConfig\n **/\n\nfunction copyCssRules(sourceDoc, targetDoc) {\n    for (const sheet of sourceDoc.styleSheets) {\n        const rules = [];\n        for (const r of sheet.cssRules) {\n            rules.push(r.cssText);\n        }\n        const cssRules = rules.join(\" \");\n        const styleTag = targetDoc.createElement(\"style\");\n        styleTag.appendChild(targetDoc.createTextNode(cssRules));\n        targetDoc.head.appendChild(styleTag);\n    }\n}\n\nexport class Wysiwyg extends Component {\n    static template = \"html_editor.Wysiwyg\";\n    static components = { Toolbar, LocalOverlayContainer };\n    static props = {\n        config: { type: Object, optional: true },\n        class: { type: String, optional: true },\n        contentClass: { type: String, optional: true }, // on editable element\n        style: { type: String, optional: true },\n        toolbar: { type: Boolean, optional: true },\n        iframe: { type: Boolean, optional: true },\n        copyCss: { type: Boolean, optional: true },\n        onLoad: { type: Function, optional: true },\n        onBlur: { type: Function, optional: true },\n        dynamicPlaceholder: { type: Boolean, optional: true },\n    };\n\n    static defaultProps = {\n        onLoad: () => {},\n        onBlur: () => {},\n    };\n\n    setup() {\n        this.state = useState({\n            showToolbar: false,\n        });\n        this.overlayRef = useChildRef();\n        useSubEnv({\n            localOverlayContainerKey: uniqueId(\"wysiwyg\"),\n        });\n        const contentRef = useRef(\"content\");\n        this.editor = this.props.editor;\n        const config = this.getEditorConfig();\n        this.editor = new Editor(config, this.env.services);\n        this.props.onLoad(this.editor);\n        useSpellCheck({\n            refName: \"content\",\n        });\n\n        onMounted(() => {\n            // now that component is mounted, editor is attached to el, and\n            // plugins are started, so we can allow the toolbar to be displayed\n            this.state.showToolbar = true;\n            /** @type { any } **/\n            const el = contentRef.el;\n\n            if (el.tagName === \"IFRAME\") {\n                // grab the inner body instead\n                const attachEditor = () => {\n                    if (!this.editor.isDestroyed) {\n                        if (this.props.copyCss) {\n                            copyCssRules(document, el.contentDocument);\n                        }\n                        const additionalClasses = el.dataset.class?.trim().split(\" \");\n                        if (additionalClasses) {\n                            for (const c of additionalClasses) {\n                                el.contentDocument.body.classList.add(c);\n                            }\n                        }\n                        this.editor.attachTo(el.contentDocument.body);\n                    }\n                };\n                if (el.contentDocument.readyState === \"complete\") {\n                    attachEditor();\n                } else {\n                    // in firefox, iframe is not immediately available. we need to wait\n                    // for it to be ready before mounting editor\n                    el.addEventListener(\n                        \"load\",\n                        () => {\n                            attachEditor();\n                            this.render();\n                        },\n                        { once: true }\n                    );\n                }\n            } else {\n                this.editor.attachTo(el);\n            }\n        });\n        onWillDestroy(() => this.editor.destroy(true));\n    }\n\n    getEditorConfig() {\n        return {\n            ...this.props.config,\n            // TODO ABD TODO @phoenix: check if there is too much info in the wysiwyg 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            embeddedComponentInfo: { app: this.__owl__.app, env: this.env },\n            localOverlayContainers: {\n                key: this.env.localOverlayContainerKey,\n                ref: this.overlayRef,\n            },\n            disableFloatingToolbar: this.props.toolbar,\n        };\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 { 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", "(function(){/*\n\n Copyright The Closure Library Authors.\n SPDX-License-Identifier: Apache-2.0\n*/\n'use strict';var D;function aa(a){var b=0;return function(){return b<a.length?{done:!1,value:a[b++]}:{done:!0}}}var ba=\"function\"==typeof Object.defineProperties?Object.defineProperty:function(a,b,c){if(a==Array.prototype||a==Object.prototype)return a;a[b]=c.value;return a};\nfunction ca(a){a=[\"object\"==typeof globalThis&&globalThis,a,\"object\"==typeof window&&window,\"object\"==typeof self&&self,\"object\"==typeof global&&global];for(var b=0;b<a.length;++b){var c=a[b];if(c&&c.Math==Math)return c}throw Error(\"Cannot find global object\");}var H=ca(this);function J(a,b){if(b)a:{var c=H;a=a.split(\".\");for(var d=0;d<a.length-1;d++){var f=a[d];if(!(f in c))break a;c=c[f]}a=a[a.length-1];d=c[a];b=b(d);b!=d&&null!=b&&ba(c,a,{configurable:!0,writable:!0,value:b})}}\nJ(\"Symbol\",function(a){function b(h){if(this instanceof b)throw new TypeError(\"Symbol is not a constructor\");return new c(d+(h||\"\")+\"_\"+f++,h)}function c(h,e){this.g=h;ba(this,\"description\",{configurable:!0,writable:!0,value:e})}if(a)return a;c.prototype.toString=function(){return this.g};var d=\"jscomp_symbol_\"+(1E9*Math.random()>>>0)+\"_\",f=0;return b});\nJ(\"Symbol.iterator\",function(a){if(a)return a;a=Symbol(\"Symbol.iterator\");for(var b=\"Array Int8Array Uint8Array Uint8ClampedArray Int16Array Uint16Array Int32Array Uint32Array Float32Array Float64Array\".split(\" \"),c=0;c<b.length;c++){var d=H[b[c]];\"function\"===typeof d&&\"function\"!=typeof d.prototype[a]&&ba(d.prototype,a,{configurable:!0,writable:!0,value:function(){return da(aa(this))}})}return a});function da(a){a={next:a};a[Symbol.iterator]=function(){return this};return a}\nfunction M(a){var b=\"undefined\"!=typeof Symbol&&Symbol.iterator&&a[Symbol.iterator];return b?b.call(a):{next:aa(a)}}function ea(a){if(!(a instanceof Array)){a=M(a);for(var b,c=[];!(b=a.next()).done;)c.push(b.value);a=c}return a}var fa=\"function\"==typeof Object.create?Object.create:function(a){function b(){}b.prototype=a;return new b},ha;\nif(\"function\"==typeof Object.setPrototypeOf)ha=Object.setPrototypeOf;else{var ia;a:{var ja={a:!0},ka={};try{ka.__proto__=ja;ia=ka.a;break a}catch(a){}ia=!1}ha=ia?function(a,b){a.__proto__=b;if(a.__proto__!==b)throw new TypeError(a+\" is not extensible\");return a}:null}var la=ha;\nfunction ma(a,b){a.prototype=fa(b.prototype);a.prototype.constructor=a;if(la)la(a,b);else for(var c in b)if(\"prototype\"!=c)if(Object.defineProperties){var d=Object.getOwnPropertyDescriptor(b,c);d&&Object.defineProperty(a,c,d)}else a[c]=b[c];a.ea=b.prototype}function na(){this.l=!1;this.i=null;this.h=void 0;this.g=1;this.s=this.m=0;this.j=null}function oa(a){if(a.l)throw new TypeError(\"Generator is already running\");a.l=!0}na.prototype.o=function(a){this.h=a};\nfunction pa(a,b){a.j={U:b,V:!0};a.g=a.m||a.s}na.prototype.return=function(a){this.j={return:a};this.g=this.s};function N(a,b,c){a.g=c;return{value:b}}function qa(a){this.g=new na;this.h=a}function ra(a,b){oa(a.g);var c=a.g.i;if(c)return sa(a,\"return\"in c?c[\"return\"]:function(d){return{value:d,done:!0}},b,a.g.return);a.g.return(b);return ta(a)}\nfunction sa(a,b,c,d){try{var f=b.call(a.g.i,c);if(!(f instanceof Object))throw new TypeError(\"Iterator result \"+f+\" is not an object\");if(!f.done)return a.g.l=!1,f;var h=f.value}catch(e){return a.g.i=null,pa(a.g,e),ta(a)}a.g.i=null;d.call(a.g,h);return ta(a)}function ta(a){for(;a.g.g;)try{var b=a.h(a.g);if(b)return a.g.l=!1,{value:b.value,done:!1}}catch(c){a.g.h=void 0,pa(a.g,c)}a.g.l=!1;if(a.g.j){b=a.g.j;a.g.j=null;if(b.V)throw b.U;return{value:b.return,done:!0}}return{value:void 0,done:!0}}\nfunction ua(a){this.next=function(b){oa(a.g);a.g.i?b=sa(a,a.g.i.next,b,a.g.o):(a.g.o(b),b=ta(a));return b};this.throw=function(b){oa(a.g);a.g.i?b=sa(a,a.g.i[\"throw\"],b,a.g.o):(pa(a.g,b),b=ta(a));return b};this.return=function(b){return ra(a,b)};this[Symbol.iterator]=function(){return this}}function O(a,b){b=new ua(new qa(b));la&&a.prototype&&la(b,a.prototype);return b}\nfunction va(a,b){a instanceof String&&(a+=\"\");var c=0,d=!1,f={next:function(){if(!d&&c<a.length){var h=c++;return{value:b(h,a[h]),done:!1}}d=!0;return{done:!0,value:void 0}}};f[Symbol.iterator]=function(){return f};return f}var wa=\"function\"==typeof Object.assign?Object.assign:function(a,b){for(var c=1;c<arguments.length;c++){var d=arguments[c];if(d)for(var f in d)Object.prototype.hasOwnProperty.call(d,f)&&(a[f]=d[f])}return a};J(\"Object.assign\",function(a){return a||wa});\nJ(\"Promise\",function(a){function b(e){this.h=0;this.i=void 0;this.g=[];this.o=!1;var g=this.j();try{e(g.resolve,g.reject)}catch(k){g.reject(k)}}function c(){this.g=null}function d(e){return e instanceof b?e:new b(function(g){g(e)})}if(a)return a;c.prototype.h=function(e){if(null==this.g){this.g=[];var g=this;this.i(function(){g.l()})}this.g.push(e)};var f=H.setTimeout;c.prototype.i=function(e){f(e,0)};c.prototype.l=function(){for(;this.g&&this.g.length;){var e=this.g;this.g=[];for(var g=0;g<e.length;++g){var k=\ne[g];e[g]=null;try{k()}catch(l){this.j(l)}}}this.g=null};c.prototype.j=function(e){this.i(function(){throw e;})};b.prototype.j=function(){function e(l){return function(q){k||(k=!0,l.call(g,q))}}var g=this,k=!1;return{resolve:e(this.C),reject:e(this.l)}};b.prototype.C=function(e){if(e===this)this.l(new TypeError(\"A Promise cannot resolve to itself\"));else if(e instanceof b)this.F(e);else{a:switch(typeof e){case \"object\":var g=null!=e;break a;case \"function\":g=!0;break a;default:g=!1}g?this.u(e):this.m(e)}};\nb.prototype.u=function(e){var g=void 0;try{g=e.then}catch(k){this.l(k);return}\"function\"==typeof g?this.G(g,e):this.m(e)};b.prototype.l=function(e){this.s(2,e)};b.prototype.m=function(e){this.s(1,e)};b.prototype.s=function(e,g){if(0!=this.h)throw Error(\"Cannot settle(\"+e+\", \"+g+\"): Promise already settled in state\"+this.h);this.h=e;this.i=g;2===this.h&&this.D();this.A()};b.prototype.D=function(){var e=this;f(function(){if(e.B()){var g=H.console;\"undefined\"!==typeof g&&g.error(e.i)}},1)};b.prototype.B=\nfunction(){if(this.o)return!1;var e=H.CustomEvent,g=H.Event,k=H.dispatchEvent;if(\"undefined\"===typeof k)return!0;\"function\"===typeof e?e=new e(\"unhandledrejection\",{cancelable:!0}):\"function\"===typeof g?e=new g(\"unhandledrejection\",{cancelable:!0}):(e=H.document.createEvent(\"CustomEvent\"),e.initCustomEvent(\"unhandledrejection\",!1,!0,e));e.promise=this;e.reason=this.i;return k(e)};b.prototype.A=function(){if(null!=this.g){for(var e=0;e<this.g.length;++e)h.h(this.g[e]);this.g=null}};var h=new c;b.prototype.F=\nfunction(e){var g=this.j();e.J(g.resolve,g.reject)};b.prototype.G=function(e,g){var k=this.j();try{e.call(g,k.resolve,k.reject)}catch(l){k.reject(l)}};b.prototype.then=function(e,g){function k(w,t){return\"function\"==typeof w?function(y){try{l(w(y))}catch(m){q(m)}}:t}var l,q,v=new b(function(w,t){l=w;q=t});this.J(k(e,l),k(g,q));return v};b.prototype.catch=function(e){return this.then(void 0,e)};b.prototype.J=function(e,g){function k(){switch(l.h){case 1:e(l.i);break;case 2:g(l.i);break;default:throw Error(\"Unexpected state: \"+\nl.h);}}var l=this;null==this.g?h.h(k):this.g.push(k);this.o=!0};b.resolve=d;b.reject=function(e){return new b(function(g,k){k(e)})};b.race=function(e){return new b(function(g,k){for(var l=M(e),q=l.next();!q.done;q=l.next())d(q.value).J(g,k)})};b.all=function(e){var g=M(e),k=g.next();return k.done?d([]):new b(function(l,q){function v(y){return function(m){w[y]=m;t--;0==t&&l(w)}}var w=[],t=0;do w.push(void 0),t++,d(k.value).J(v(w.length-1),q),k=g.next();while(!k.done)})};return b});\nJ(\"Object.is\",function(a){return a?a:function(b,c){return b===c?0!==b||1/b===1/c:b!==b&&c!==c}});J(\"Array.prototype.includes\",function(a){return a?a:function(b,c){var d=this;d instanceof String&&(d=String(d));var f=d.length;c=c||0;for(0>c&&(c=Math.max(c+f,0));c<f;c++){var h=d[c];if(h===b||Object.is(h,b))return!0}return!1}});\nJ(\"String.prototype.includes\",function(a){return a?a:function(b,c){if(null==this)throw new TypeError(\"The 'this' value for String.prototype.includes must not be null or undefined\");if(b instanceof RegExp)throw new TypeError(\"First argument to String.prototype.includes must not be a regular expression\");return-1!==this.indexOf(b,c||0)}});J(\"Array.prototype.keys\",function(a){return a?a:function(){return va(this,function(b){return b})}});var xa=this||self;\nfunction ya(a,b){a=a.split(\".\");var c=xa;a[0]in c||\"undefined\"==typeof c.execScript||c.execScript(\"var \"+a[0]);for(var d;a.length&&(d=a.shift());)a.length||void 0===b?c[d]&&c[d]!==Object.prototype[d]?c=c[d]:c=c[d]={}:c[d]=b};function za(a,b){b=String.fromCharCode.apply(null,b);return null==a?b:a+b}var Aa,Ba=\"undefined\"!==typeof TextDecoder,Ca,Da=\"undefined\"!==typeof TextEncoder;\nfunction Ea(a){if(Da)a=(Ca||(Ca=new TextEncoder)).encode(a);else{var b=void 0;b=void 0===b?!1:b;for(var c=0,d=new Uint8Array(3*a.length),f=0;f<a.length;f++){var h=a.charCodeAt(f);if(128>h)d[c++]=h;else{if(2048>h)d[c++]=h>>6|192;else{if(55296<=h&&57343>=h){if(56319>=h&&f<a.length){var e=a.charCodeAt(++f);if(56320<=e&&57343>=e){h=1024*(h-55296)+e-56320+65536;d[c++]=h>>18|240;d[c++]=h>>12&63|128;d[c++]=h>>6&63|128;d[c++]=h&63|128;continue}else f--}if(b)throw Error(\"Found an unpaired surrogate\");h=65533}d[c++]=\nh>>12|224;d[c++]=h>>6&63|128}d[c++]=h&63|128}}a=d.subarray(0,c)}return a};var Fa={},Ga=null;function Ha(a,b){void 0===b&&(b=0);Ja();b=Fa[b];for(var c=Array(Math.floor(a.length/3)),d=b[64]||\"\",f=0,h=0;f<a.length-2;f+=3){var e=a[f],g=a[f+1],k=a[f+2],l=b[e>>2];e=b[(e&3)<<4|g>>4];g=b[(g&15)<<2|k>>6];k=b[k&63];c[h++]=l+e+g+k}l=0;k=d;switch(a.length-f){case 2:l=a[f+1],k=b[(l&15)<<2]||d;case 1:a=a[f],c[h]=b[a>>2]+b[(a&3)<<4|l>>4]+k+d}return c.join(\"\")}\nfunction Ka(a){var b=a.length,c=3*b/4;c%3?c=Math.floor(c):-1!=\"=.\".indexOf(a[b-1])&&(c=-1!=\"=.\".indexOf(a[b-2])?c-2:c-1);var d=new Uint8Array(c),f=0;La(a,function(h){d[f++]=h});return d.subarray(0,f)}\nfunction La(a,b){function c(k){for(;d<a.length;){var l=a.charAt(d++),q=Ga[l];if(null!=q)return q;if(!/^[\\s\\xa0]*$/.test(l))throw Error(\"Unknown base64 encoding at char: \"+l);}return k}Ja();for(var d=0;;){var f=c(-1),h=c(0),e=c(64),g=c(64);if(64===g&&-1===f)break;b(f<<2|h>>4);64!=e&&(b(h<<4&240|e>>2),64!=g&&b(e<<6&192|g))}}\nfunction Ja(){if(!Ga){Ga={};for(var a=\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\".split(\"\"),b=[\"+/=\",\"+/\",\"-_=\",\"-_.\",\"-_\"],c=0;5>c;c++){var d=a.concat(b[c].split(\"\"));Fa[c]=d;for(var f=0;f<d.length;f++){var h=d[f];void 0===Ga[h]&&(Ga[h]=f)}}}};var Ma=\"function\"===typeof Uint8Array.prototype.slice,Na;function Oa(a,b,c){return b===c?Na||(Na=new Uint8Array(0)):Ma?a.slice(b,c):new Uint8Array(a.subarray(b,c))}var P=0,Q=0;function Pa(a,b){b=void 0===b?{}:b;b=void 0===b.v?!1:b.v;this.h=null;this.g=this.i=this.j=0;this.l=!1;this.v=b;a&&Qa(this,a)}function Qa(a,b){b=b.constructor===Uint8Array?b:b.constructor===ArrayBuffer?new Uint8Array(b):b.constructor===Array?new Uint8Array(b):b.constructor===String?Ka(b):b instanceof Uint8Array?new Uint8Array(b.buffer,b.byteOffset,b.byteLength):new Uint8Array(0);a.h=b;a.j=0;a.i=a.h.length;a.g=a.j}Pa.prototype.reset=function(){this.g=this.j};\nfunction Ra(a){var b=a.h,c=b[a.g],d=c&127;if(128>c)return a.g+=1,d;c=b[a.g+1];d|=(c&127)<<7;if(128>c)return a.g+=2,d;c=b[a.g+2];d|=(c&127)<<14;if(128>c)return a.g+=3,d;c=b[a.g+3];d|=(c&127)<<21;if(128>c)return a.g+=4,d;c=b[a.g+4];d|=(c&15)<<28;if(128>c)return a.g+=5,d>>>0;a.g+=5;128<=b[a.g++]&&128<=b[a.g++]&&128<=b[a.g++]&&128<=b[a.g++]&&a.g++;return d}\nfunction R(a){var b=a.h[a.g];var c=a.h[a.g+1];var d=a.h[a.g+2],f=a.h[a.g+3];a.g+=4;c=(b<<0|c<<8|d<<16|f<<24)>>>0;a=2*(c>>31)+1;b=c>>>23&255;c&=8388607;return 255==b?c?NaN:Infinity*a:0==b?a*Math.pow(2,-149)*c:a*Math.pow(2,b-150)*(c+Math.pow(2,23))}var Sa=[];function Ta(){this.g=new Uint8Array(64);this.h=0}Ta.prototype.push=function(a){if(!(this.h+1<this.g.length)){var b=this.g;this.g=new Uint8Array(Math.ceil(1+2*this.g.length));this.g.set(b)}this.g[this.h++]=a};Ta.prototype.length=function(){return this.h};Ta.prototype.end=function(){var a=this.g,b=this.h;this.h=0;return Oa(a,0,b)};function S(a,b){for(;127<b;)a.push(b&127|128),b>>>=7;a.push(b)};function Ua(a){var b={},c=void 0===b.N?!1:b.N;this.o={v:void 0===b.v?!1:b.v};this.N=c;b=this.o;Sa.length?(c=Sa.pop(),b&&(c.v=b.v),a&&Qa(c,a),a=c):a=new Pa(a,b);this.g=a;this.m=this.g.g;this.h=this.i=this.l=-1;this.j=!1}Ua.prototype.reset=function(){this.g.reset();this.h=this.l=-1};function Va(a){var b=a.g;(b=b.g==b.i)||(b=a.j)||(b=a.g,b=b.l||0>b.g||b.g>b.i);if(b)return!1;a.m=a.g.g;b=Ra(a.g);var c=b&7;if(0!=c&&5!=c&&1!=c&&2!=c&&3!=c&&4!=c)return a.j=!0,!1;a.i=b;a.l=b>>>3;a.h=c;return!0}\nfunction Wa(a){switch(a.h){case 0:if(0!=a.h)Wa(a);else{for(a=a.g;a.h[a.g]&128;)a.g++;a.g++}break;case 1:1!=a.h?Wa(a):(a=a.g,a.g+=8);break;case 2:if(2!=a.h)Wa(a);else{var b=Ra(a.g);a=a.g;a.g+=b}break;case 5:5!=a.h?Wa(a):(a=a.g,a.g+=4);break;case 3:b=a.l;do{if(!Va(a)){a.j=!0;break}if(4==a.h){a.l!=b&&(a.j=!0);break}Wa(a)}while(1);break;default:a.j=!0}}function Xa(a,b,c){var d=a.g.i,f=Ra(a.g);f=a.g.g+f;a.g.i=f;c(b,a);a.g.g=f;a.g.i=d;return b}\nfunction Ya(a){var b=Ra(a.g);a=a.g;var c=a.g;a.g+=b;a=a.h;var d;if(Ba)(d=Aa)||(d=Aa=new TextDecoder(\"utf-8\",{fatal:!1})),d=d.decode(a.subarray(c,c+b));else{b=c+b;for(var f=[],h=null,e,g,k;c<b;)e=a[c++],128>e?f.push(e):224>e?c>=b?f.push(65533):(g=a[c++],194>e||128!==(g&192)?(c--,f.push(65533)):f.push((e&31)<<6|g&63)):240>e?c>=b-1?f.push(65533):(g=a[c++],128!==(g&192)||224===e&&160>g||237===e&&160<=g||128!==((d=a[c++])&192)?(c--,f.push(65533)):f.push((e&15)<<12|(g&63)<<6|d&63)):244>=e?c>=b-2?f.push(65533):\n(g=a[c++],128!==(g&192)||0!==(e<<28)+(g-144)>>30||128!==((d=a[c++])&192)||128!==((k=a[c++])&192)?(c--,f.push(65533)):(e=(e&7)<<18|(g&63)<<12|(d&63)<<6|k&63,e-=65536,f.push((e>>10&1023)+55296,(e&1023)+56320))):f.push(65533),8192<=f.length&&(h=za(h,f),f.length=0);d=za(h,f)}return d};function Za(){this.h=[];this.i=0;this.g=new Ta}function $a(a,b){0!==b.length&&(a.h.push(b),a.i+=b.length)}function ab(a){var b=a.i+a.g.length();if(0===b)return new Uint8Array(0);b=new Uint8Array(b);for(var c=a.h,d=c.length,f=0,h=0;h<d;h++){var e=c[h];0!==e.length&&(b.set(e,f),f+=e.length)}c=a.g;d=c.h;0!==d&&(b.set(c.g.subarray(0,d),f),c.h=0);a.h=[b];return b}\nfunction T(a,b,c){if(null!=c){S(a.g,8*b+5);a=a.g;var d=c;d=(c=0>d?1:0)?-d:d;0===d?0<1/d?P=Q=0:(Q=0,P=2147483648):isNaN(d)?(Q=0,P=2147483647):3.4028234663852886E38<d?(Q=0,P=(c<<31|2139095040)>>>0):1.1754943508222875E-38>d?(d=Math.round(d/Math.pow(2,-149)),Q=0,P=(c<<31|d)>>>0):(b=Math.floor(Math.log(d)/Math.LN2),d*=Math.pow(2,-b),d=Math.round(8388608*d)&8388607,Q=0,P=(c<<31|b+127<<23|d)>>>0);c=P;a.push(c>>>0&255);a.push(c>>>8&255);a.push(c>>>16&255);a.push(c>>>24&255)}};var bb=\"function\"===typeof Uint8Array;function cb(a,b,c){if(null!=a)return\"object\"===typeof a?bb&&a instanceof Uint8Array?c(a):db(a,b,c):b(a)}function db(a,b,c){if(Array.isArray(a)){for(var d=Array(a.length),f=0;f<a.length;f++)d[f]=cb(a[f],b,c);Array.isArray(a)&&a.W&&eb(d);return d}d={};for(f in a)d[f]=cb(a[f],b,c);return d}function fb(a){return\"number\"===typeof a?isFinite(a)?a:String(a):a}var gb={W:{value:!0,configurable:!0}};\nfunction eb(a){Array.isArray(a)&&!Object.isFrozen(a)&&Object.defineProperties(a,gb);return a};var hb;function U(a,b,c){var d=hb;hb=null;a||(a=d);d=this.constructor.ca;a||(a=d?[d]:[]);this.j=d?0:-1;this.i=null;this.g=a;a:{d=this.g.length;a=d-1;if(d&&(d=this.g[a],null!==d&&\"object\"===typeof d&&d.constructor===Object)){this.l=a-this.j;this.h=d;break a}void 0!==b&&-1<b?(this.l=Math.max(b,a+1-this.j),this.h=null):this.l=Number.MAX_VALUE}if(c)for(b=0;b<c.length;b++)a=c[b],a<this.l?(a+=this.j,(d=this.g[a])?eb(d):this.g[a]=ib):(jb(this),(d=this.h[a])?eb(d):this.h[a]=ib)}var ib=Object.freeze(eb([]));\nfunction jb(a){var b=a.l+a.j;a.g[b]||(a.h=a.g[b]={})}function V(a,b,c){return-1===b?null:(void 0===c?0:c)||b>=a.l?a.h?a.h[b]:void 0:a.g[b+a.j]}function kb(a){var b=void 0===b?!1:b;var c=V(a,1,b);null==c&&(c=ib);c===ib&&(c=eb([]),W(a,1,c,b));return c}function X(a,b,c){a=V(a,b);a=null==a?a:+a;return null==a?void 0===c?0:c:a}function W(a,b,c,d){(void 0===d?0:d)||b>=a.l?(jb(a),a.h[b]=c):a.g[b+a.j]=c}\nfunction lb(a,b){a.i||(a.i={});var c=a.i[1];if(!c){var d=kb(a);c=[];for(var f=0;f<d.length;f++)c[f]=new b(d[f]);a.i[1]=c}return c}function mb(a,b,c,d){var f=lb(a,c);b=b?b:new c;a=kb(a);void 0!=d?(f.splice(d,0,b),a.splice(d,0,nb(b,!1))):(f.push(b),a.push(nb(b,!1)))}U.prototype.toJSON=function(){var a=nb(this,!1);return db(a,fb,Ha)};function nb(a,b){if(a.i)for(var c in a.i){var d=a.i[c];if(Array.isArray(d))for(var f=0;f<d.length;f++)d[f]&&nb(d[f],b);else d&&nb(d,b)}return a.g}\nU.prototype.toString=function(){return nb(this,!1).toString()};function ob(a,b){a=V(a,b);return null==a?0:a}function pb(a,b){a=V(a,b);return null==a?\"\":a};function qb(a,b){if(a=a.m){$a(b,b.g.end());for(var c=0;c<a.length;c++)$a(b,a[c])}}function rb(a,b){if(4==b.h)return!1;var c=b.m;Wa(b);b.N||(b=Oa(b.g.h,c,b.g.g),(c=a.m)?c.push(b):a.m=[b]);return!0};function Y(a,b){var c=void 0;return new (c||(c=Promise))(function(d,f){function h(k){try{g(b.next(k))}catch(l){f(l)}}function e(k){try{g(b[\"throw\"](k))}catch(l){f(l)}}function g(k){k.done?d(k.value):(new c(function(l){l(k.value)})).then(h,e)}g((b=b.apply(a,void 0)).next())})};function sb(a){U.call(this,a)}ma(sb,U);function tb(a,b){for(;Va(b);)switch(b.i){case 8:var c=Ra(b.g);W(a,1,c);break;case 21:c=R(b.g);W(a,2,c);break;case 26:c=Ya(b);W(a,3,c);break;case 34:c=Ya(b);W(a,4,c);break;default:if(!rb(a,b))return a}return a};function ub(a){U.call(this,a,-1,vb)}ma(ub,U);ub.prototype.addClassification=function(a,b){mb(this,a,sb,b)};var vb=[1];function wb(a){U.call(this,a)}ma(wb,U);function xb(a,b){for(;Va(b);)switch(b.i){case 13:var c=R(b.g);W(a,1,c);break;case 21:c=R(b.g);W(a,2,c);break;case 29:c=R(b.g);W(a,3,c);break;case 37:c=R(b.g);W(a,4,c);break;case 45:c=R(b.g);W(a,5,c);break;default:if(!rb(a,b))return a}return a};function yb(a){U.call(this,a,-1,zb)}ma(yb,U);var zb=[1];function Ab(a){U.call(this,a)}ma(Ab,U);function Bb(a,b,c){c=a.createShader(0===c?a.VERTEX_SHADER:a.FRAGMENT_SHADER);a.shaderSource(c,b);a.compileShader(c);if(!a.getShaderParameter(c,a.COMPILE_STATUS))throw Error(\"Could not compile WebGL shader.\\n\\n\"+a.getShaderInfoLog(c));return c};function Cb(a){return lb(a,sb).map(function(b){return{index:ob(b,1),Y:X(b,2),label:null!=V(b,3)?pb(b,3):void 0,displayName:null!=V(b,4)?pb(b,4):void 0}})};function Db(a){return{x:X(a,1),y:X(a,2),z:X(a,3),visibility:null!=V(a,4)?X(a,4):void 0}};function Eb(a,b){this.h=a;this.g=b;this.l=0}\nfunction Fb(a,b,c){Gb(a,b);if(\"function\"===typeof a.g.canvas.transferToImageBitmap)return Promise.resolve(a.g.canvas.transferToImageBitmap());if(c)return Promise.resolve(a.g.canvas);if(\"function\"===typeof createImageBitmap)return createImageBitmap(a.g.canvas);void 0===a.i&&(a.i=document.createElement(\"canvas\"));return new Promise(function(d){a.i.height=a.g.canvas.height;a.i.width=a.g.canvas.width;a.i.getContext(\"2d\",{}).drawImage(a.g.canvas,0,0,a.g.canvas.width,a.g.canvas.height);d(a.i)})}\nfunction Gb(a,b){var c=a.g;if(void 0===a.m){var d=Bb(c,\"\\n  attribute vec2 aVertex;\\n  attribute vec2 aTex;\\n  varying vec2 vTex;\\n  void main(void) {\\n    gl_Position = vec4(aVertex, 0.0, 1.0);\\n    vTex = aTex;\\n  }\",0),f=Bb(c,\"\\n  precision mediump float;\\n  varying vec2 vTex;\\n  uniform sampler2D sampler0;\\n  void main(){\\n    gl_FragColor = texture2D(sampler0, vTex);\\n  }\",1),h=c.createProgram();c.attachShader(h,d);c.attachShader(h,f);c.linkProgram(h);if(!c.getProgramParameter(h,c.LINK_STATUS))throw Error(\"Could not compile WebGL program.\\n\\n\"+\nc.getProgramInfoLog(h));d=a.m=h;c.useProgram(d);f=c.getUniformLocation(d,\"sampler0\");a.j={I:c.getAttribLocation(d,\"aVertex\"),H:c.getAttribLocation(d,\"aTex\"),da:f};a.s=c.createBuffer();c.bindBuffer(c.ARRAY_BUFFER,a.s);c.enableVertexAttribArray(a.j.I);c.vertexAttribPointer(a.j.I,2,c.FLOAT,!1,0,0);c.bufferData(c.ARRAY_BUFFER,new Float32Array([-1,-1,-1,1,1,1,1,-1]),c.STATIC_DRAW);c.bindBuffer(c.ARRAY_BUFFER,null);a.o=c.createBuffer();c.bindBuffer(c.ARRAY_BUFFER,a.o);c.enableVertexAttribArray(a.j.H);c.vertexAttribPointer(a.j.H,\n2,c.FLOAT,!1,0,0);c.bufferData(c.ARRAY_BUFFER,new Float32Array([0,1,0,0,1,0,1,1]),c.STATIC_DRAW);c.bindBuffer(c.ARRAY_BUFFER,null);c.uniform1i(f,0)}d=a.j;c.useProgram(a.m);c.canvas.width=b.width;c.canvas.height=b.height;c.viewport(0,0,b.width,b.height);c.activeTexture(c.TEXTURE0);a.h.bindTexture2d(b.glName);c.enableVertexAttribArray(d.I);c.bindBuffer(c.ARRAY_BUFFER,a.s);c.vertexAttribPointer(d.I,2,c.FLOAT,!1,0,0);c.enableVertexAttribArray(d.H);c.bindBuffer(c.ARRAY_BUFFER,a.o);c.vertexAttribPointer(d.H,\n2,c.FLOAT,!1,0,0);c.bindFramebuffer(c.DRAW_FRAMEBUFFER?c.DRAW_FRAMEBUFFER:c.FRAMEBUFFER,null);c.clearColor(0,0,0,0);c.clear(c.COLOR_BUFFER_BIT);c.colorMask(!0,!0,!0,!0);c.drawArrays(c.TRIANGLE_FAN,0,4);c.disableVertexAttribArray(d.I);c.disableVertexAttribArray(d.H);c.bindBuffer(c.ARRAY_BUFFER,null);a.h.bindTexture2d(0)}function Hb(a){this.g=a};var Ib=new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,9,1,7,0,65,0,253,15,26,11]);function Jb(a,b){return b+a}function Kb(a,b){window[a]=b}function Lb(a){var b=document.createElement(\"script\");b.setAttribute(\"src\",a);b.setAttribute(\"crossorigin\",\"anonymous\");return new Promise(function(c){b.addEventListener(\"load\",function(){c()},!1);b.addEventListener(\"error\",function(){c()},!1);document.body.appendChild(b)})}\nfunction Mb(){return Y(this,function b(){return O(b,function(c){switch(c.g){case 1:return c.m=2,N(c,WebAssembly.instantiate(Ib),4);case 4:c.g=3;c.m=0;break;case 2:return c.m=0,c.j=null,c.return(!1);case 3:return c.return(!0)}})})}\nfunction Nb(a){this.g=a;this.listeners={};this.j={};this.F={};this.m={};this.s={};this.G=this.o=this.R=!0;this.C=Promise.resolve();this.P=\"\";this.B={};this.locateFile=a&&a.locateFile||Jb;if(\"object\"===typeof window)var b=window.location.pathname.toString().substring(0,window.location.pathname.toString().lastIndexOf(\"/\"))+\"/\";else if(\"undefined\"!==typeof location)b=location.pathname.toString().substring(0,location.pathname.toString().lastIndexOf(\"/\"))+\"/\";else throw Error(\"solutions can only be loaded on a web page or in a web worker\");\nthis.S=b;if(a.options){b=M(Object.keys(a.options));for(var c=b.next();!c.done;c=b.next()){c=c.value;var d=a.options[c].default;void 0!==d&&(this.j[c]=\"function\"===typeof d?d():d)}}}D=Nb.prototype;D.close=function(){this.i&&this.i.delete();return Promise.resolve()};function Ob(a,b){return void 0===a.g.files?[]:\"function\"===typeof a.g.files?a.g.files(b):a.g.files}\nfunction Pb(a){return Y(a,function c(){var d=this,f,h,e,g,k,l,q,v,w,t,y;return O(c,function(m){switch(m.g){case 1:f=d;if(!d.R)return m.return();h=Ob(d,d.j);return N(m,Mb(),2);case 2:e=m.h;if(\"object\"===typeof window)return Kb(\"createMediapipeSolutionsWasm\",{locateFile:d.locateFile}),Kb(\"createMediapipeSolutionsPackedAssets\",{locateFile:d.locateFile}),l=h.filter(function(u){return void 0!==u.data}),q=h.filter(function(u){return void 0===u.data}),v=Promise.all(l.map(function(u){var x=Qb(f,u.url);if(void 0!==\nu.path){var z=u.path;x=x.then(function(E){f.overrideFile(z,E);return Promise.resolve(E)})}return x})),w=Promise.all(q.map(function(u){return void 0===u.simd||u.simd&&e||!u.simd&&!e?Lb(f.locateFile(u.url,f.S)):Promise.resolve()})).then(function(){return Y(f,function x(){var z,E,F=this;return O(x,function(K){if(1==K.g)return z=window.createMediapipeSolutionsWasm,E=window.createMediapipeSolutionsPackedAssets,N(K,z(E),2);F.h=K.h;K.g=0})})}),t=function(){return Y(f,function x(){var z=this;return O(x,function(E){z.g.graph&&\nz.g.graph.url?E=N(E,Qb(z,z.g.graph.url),0):(E.g=0,E=void 0);return E})})}(),N(m,Promise.all([w,v,t]),7);if(\"function\"!==typeof importScripts)throw Error(\"solutions can only be loaded on a web page or in a web worker\");g=h.filter(function(u){return void 0===u.simd||u.simd&&e||!u.simd&&!e}).map(function(u){return f.locateFile(u.url,f.S)});importScripts.apply(null,ea(g));return N(m,createMediapipeSolutionsWasm(Module),6);case 6:d.h=m.h;d.l=new OffscreenCanvas(1,1);d.h.canvas=d.l;k=d.h.GL.createContext(d.l,\n{antialias:!1,alpha:!1,ba:\"undefined\"!==typeof WebGL2RenderingContext?2:1});d.h.GL.makeContextCurrent(k);m.g=4;break;case 7:d.l=document.createElement(\"canvas\");y=d.l.getContext(\"webgl2\",{});if(!y&&(y=d.l.getContext(\"webgl\",{}),!y))return alert(\"Failed to create WebGL canvas context when passing video frame.\"),m.return();d.D=y;d.h.canvas=d.l;d.h.createContext(d.l,!0,!0,{});case 4:d.i=new d.h.SolutionWasm,d.R=!1,m.g=0}})})}\nfunction Rb(a){return Y(a,function c(){var d=this,f,h,e,g,k,l,q,v;return O(c,function(w){if(1==w.g){if(d.g.graph&&d.g.graph.url&&d.P===d.g.graph.url)return w.return();d.o=!0;if(!d.g.graph||!d.g.graph.url){w.g=2;return}d.P=d.g.graph.url;return N(w,Qb(d,d.g.graph.url),3)}2!=w.g&&(f=w.h,d.i.loadGraph(f));h=M(Object.keys(d.B));for(e=h.next();!e.done;e=h.next())g=e.value,d.i.overrideFile(g,d.B[g]);d.B={};if(d.g.listeners)for(k=M(d.g.listeners),l=k.next();!l.done;l=k.next())q=l.value,Sb(d,q);v=d.j;d.j=\n{};d.setOptions(v);w.g=0})})}D.reset=function(){return Y(this,function b(){var c=this;return O(b,function(d){c.i&&(c.i.reset(),c.m={},c.s={});d.g=0})})};\nD.setOptions=function(a,b){var c=this;if(b=b||this.g.options){for(var d=[],f=[],h={},e=M(Object.keys(a)),g=e.next();!g.done;h={K:h.K,L:h.L},g=e.next()){var k=g.value;k in this.j&&this.j[k]===a[k]||(this.j[k]=a[k],g=b[k],void 0!==g&&(g.onChange&&(h.K=g.onChange,h.L=a[k],d.push(function(l){return function(){return Y(c,function v(){var w,t=this;return O(v,function(y){if(1==y.g)return N(y,l.K(l.L),2);w=y.h;!0===w&&(t.o=!0);y.g=0})})}}(h))),g.graphOptionXref&&(k={valueNumber:1===g.type?a[k]:0,valueBoolean:0===\ng.type?a[k]:!1,valueString:2===g.type?a[k]:\"\"},g=Object.assign(Object.assign(Object.assign({},{calculatorName:\"\",calculatorIndex:0}),g.graphOptionXref),k),f.push(g))))}if(0!==d.length||0!==f.length)this.o=!0,this.A=(void 0===this.A?[]:this.A).concat(f),this.u=(void 0===this.u?[]:this.u).concat(d)}};\nfunction Tb(a){return Y(a,function c(){var d=this,f,h,e,g,k,l,q;return O(c,function(v){switch(v.g){case 1:if(!d.o)return v.return();if(!d.u){v.g=2;break}f=M(d.u);h=f.next();case 3:if(h.done){v.g=5;break}e=h.value;return N(v,e(),4);case 4:h=f.next();v.g=3;break;case 5:d.u=void 0;case 2:if(d.A){g=new d.h.GraphOptionChangeRequestList;k=M(d.A);for(l=k.next();!l.done;l=k.next())q=l.value,g.push_back(q);d.i.changeOptions(g);g.delete();d.A=void 0}d.o=!1;v.g=0}})})}\nD.initialize=function(){return Y(this,function b(){var c=this;return O(b,function(d){return 1==d.g?N(d,Pb(c),2):3!=d.g?N(d,Rb(c),3):N(d,Tb(c),0)})})};function Qb(a,b){return Y(a,function d(){var f=this,h,e;return O(d,function(g){if(b in f.F)return g.return(f.F[b]);h=f.locateFile(b,\"\");e=fetch(h).then(function(k){return k.arrayBuffer()});f.F[b]=e;return g.return(e)})})}D.overrideFile=function(a,b){this.i?this.i.overrideFile(a,b):this.B[a]=b};D.clearOverriddenFiles=function(){this.B={};this.i&&this.i.clearOverriddenFiles()};\nD.send=function(a,b){return Y(this,function d(){var f=this,h,e,g,k,l,q,v,w,t;return O(d,function(y){switch(y.g){case 1:if(!f.g.inputs)return y.return();h=1E3*(void 0===b||null===b?performance.now():b);return N(y,f.C,2);case 2:return N(y,f.initialize(),3);case 3:e=new f.h.PacketDataList;g=M(Object.keys(a));for(k=g.next();!k.done;k=g.next())if(l=k.value,q=f.g.inputs[l]){a:{var m=f;var u=a[l];switch(q.type){case \"video\":var x=m.m[q.stream];x||(x=new Eb(m.h,m.D),m.m[q.stream]=x);m=x;0===m.l&&(m.l=m.h.createTexture());\nif(\"undefined\"!==typeof HTMLVideoElement&&u instanceof HTMLVideoElement){var z=u.videoWidth;x=u.videoHeight}else\"undefined\"!==typeof HTMLImageElement&&u instanceof HTMLImageElement?(z=u.naturalWidth,x=u.naturalHeight):(z=u.width,x=u.height);x={glName:m.l,width:z,height:x};z=m.g;z.canvas.width=x.width;z.canvas.height=x.height;z.activeTexture(z.TEXTURE0);m.h.bindTexture2d(m.l);z.texImage2D(z.TEXTURE_2D,0,z.RGBA,z.RGBA,z.UNSIGNED_BYTE,u);m.h.bindTexture2d(0);m=x;break a;case \"detections\":x=m.m[q.stream];\nx||(x=new Hb(m.h),m.m[q.stream]=x);m=x;m.data||(m.data=new m.g.DetectionListData);m.data.reset(u.length);for(x=0;x<u.length;++x){z=u[x];var E=m.data,F=E.setBoundingBox,K=x;var I=z.T;var r=new Ab;W(r,1,I.Z);W(r,2,I.$);W(r,3,I.height);W(r,4,I.width);W(r,5,I.rotation);W(r,6,I.X);var A=I=new Za;T(A,1,V(r,1));T(A,2,V(r,2));T(A,3,V(r,3));T(A,4,V(r,4));T(A,5,V(r,5));var C=V(r,6);if(null!=C&&null!=C){S(A.g,48);var p=A.g,B=C;C=0>B;B=Math.abs(B);var n=B>>>0;B=Math.floor((B-n)/4294967296);B>>>=0;C&&(B=~B>>>\n0,n=(~n>>>0)+1,4294967295<n&&(n=0,B++,4294967295<B&&(B=0)));P=n;Q=B;C=P;for(n=Q;0<n||127<C;)p.push(C&127|128),C=(C>>>7|n<<25)>>>0,n>>>=7;p.push(C)}qb(r,A);I=ab(I);F.call(E,K,I);if(z.O)for(E=0;E<z.O.length;++E)r=z.O[E],A=r.visibility?!0:!1,F=m.data,K=F.addNormalizedLandmark,I=x,r=Object.assign(Object.assign({},r),{visibility:A?r.visibility:0}),A=new wb,W(A,1,r.x),W(A,2,r.y),W(A,3,r.z),r.visibility&&W(A,4,r.visibility),p=r=new Za,T(p,1,V(A,1)),T(p,2,V(A,2)),T(p,3,V(A,3)),T(p,4,V(A,4)),T(p,5,V(A,5)),\nqb(A,p),r=ab(r),K.call(F,I,r);if(z.M)for(E=0;E<z.M.length;++E){F=m.data;K=F.addClassification;I=x;r=z.M[E];A=new sb;W(A,2,r.Y);r.index&&W(A,1,r.index);r.label&&W(A,3,r.label);r.displayName&&W(A,4,r.displayName);p=r=new Za;n=V(A,1);if(null!=n&&null!=n)if(S(p.g,8),C=p.g,0<=n)S(C,n);else{for(B=0;9>B;B++)C.push(n&127|128),n>>=7;C.push(1)}T(p,2,V(A,2));C=V(A,3);null!=C&&(C=Ea(C),S(p.g,26),S(p.g,C.length),$a(p,p.g.end()),$a(p,C));C=V(A,4);null!=C&&(C=Ea(C),S(p.g,34),S(p.g,C.length),$a(p,p.g.end()),$a(p,\nC));qb(A,p);r=ab(r);K.call(F,I,r)}}m=m.data;break a;default:m={}}}v=m;w=q.stream;switch(q.type){case \"video\":e.pushTexture2d(Object.assign(Object.assign({},v),{stream:w,timestamp:h}));break;case \"detections\":t=v;t.stream=w;t.timestamp=h;e.pushDetectionList(t);break;default:throw Error(\"Unknown input config type: '\"+q.type+\"'\");}}f.i.send(e);return N(y,f.C,4);case 4:e.delete(),y.g=0}})})};\nfunction Ub(a,b,c){return Y(a,function f(){var h,e,g,k,l,q,v=this,w,t,y,m,u,x,z,E;return O(f,function(F){switch(F.g){case 1:if(!c)return F.return(b);h={};e=0;g=M(Object.keys(c));for(k=g.next();!k.done;k=g.next())l=k.value,q=c[l],\"string\"!==typeof q&&\"texture\"===q.type&&void 0!==b[q.stream]&&++e;1<e&&(v.G=!1);w=M(Object.keys(c));k=w.next();case 2:if(k.done){F.g=4;break}t=k.value;y=c[t];if(\"string\"===typeof y)return z=h,E=t,N(F,Vb(v,t,b[y]),14);m=b[y.stream];if(\"detection_list\"===y.type){if(m){var K=\nm.getRectList();for(var I=m.getLandmarksList(),r=m.getClassificationsList(),A=[],C=0;C<K.size();++C){var p=K.get(C);a:{var B=new Ab;for(p=new Ua(p);Va(p);)switch(p.i){case 13:var n=R(p.g);W(B,1,n);break;case 21:n=R(p.g);W(B,2,n);break;case 29:n=R(p.g);W(B,3,n);break;case 37:n=R(p.g);W(B,4,n);break;case 45:n=R(p.g);W(B,5,n);break;case 48:for(var G=p.g,L=128,Ia=0,Z=n=0;4>Z&&128<=L;Z++)L=G.h[G.g++],Ia|=(L&127)<<7*Z;128<=L&&(L=G.h[G.g++],Ia|=(L&127)<<28,n|=(L&127)>>4);if(128<=L)for(Z=0;5>Z&&128<=L;Z++)L=\nG.h[G.g++],n|=(L&127)<<7*Z+3;if(128>L){G=Ia>>>0;L=n>>>0;if(n=L&2147483648)G=~G+1>>>0,L=~L>>>0,0==G&&(L=L+1>>>0);G=4294967296*L+(G>>>0);n=n?-G:G}else G.l=!0,n=void 0;W(B,6,n);break;default:if(!rb(B,p))break a}}B={Z:X(B,1),$:X(B,2),height:X(B,3),width:X(B,4),rotation:X(B,5,0),X:ob(B,6)};n=I.get(C);a:for(p=new yb,n=new Ua(n);Va(n);)switch(n.i){case 10:G=Xa(n,new wb,xb);mb(p,G,wb,void 0);break;default:if(!rb(p,n))break a}p=lb(p,wb).map(Db);G=r.get(C);a:for(n=new ub,G=new Ua(G);Va(G);)switch(G.i){case 10:n.addClassification(Xa(G,\nnew sb,tb));break;default:if(!rb(n,G))break a}B={T:B,O:p,M:Cb(n)};A.push(B)}K=A}else K=[];h[t]=K;F.g=7;break}if(\"proto_list\"===y.type){if(m){K=Array(m.size());for(I=0;I<m.size();I++)K[I]=m.get(I);m.delete()}else K=[];h[t]=K;F.g=7;break}if(void 0===m){F.g=3;break}if(\"float_list\"===y.type){h[t]=m;F.g=7;break}if(\"proto\"===y.type){h[t]=m;F.g=7;break}if(\"texture\"!==y.type)throw Error(\"Unknown output config type: '\"+y.type+\"'\");u=v.s[t];u||(u=new Eb(v.h,v.D),v.s[t]=u);return N(F,Fb(u,m,v.G),13);case 13:x=\nF.h,h[t]=x;case 7:y.transform&&h[t]&&(h[t]=y.transform(h[t]));F.g=3;break;case 14:z[E]=F.h;case 3:k=w.next();F.g=2;break;case 4:return F.return(h)}})})}function Vb(a,b,c){return Y(a,function f(){var h=this,e;return O(f,function(g){return\"number\"===typeof c||c instanceof Uint8Array||c instanceof h.h.Uint8BlobList?g.return(c):c instanceof h.h.Texture2dDataOut?(e=h.s[b],e||(e=new Eb(h.h,h.D),h.s[b]=e),g.return(Fb(e,c,h.G))):g.return(void 0)})})}\nfunction Sb(a,b){for(var c=b.name||\"$\",d=[].concat(ea(b.wants)),f=new a.h.StringList,h=M(b.wants),e=h.next();!e.done;e=h.next())f.push_back(e.value);h=a.h.PacketListener.implement({onResults:function(g){for(var k={},l=0;l<b.wants.length;++l)k[d[l]]=g.get(l);var q=a.listeners[c];q&&(a.C=Ub(a,k,b.outs).then(function(v){v=q(v);for(var w=0;w<b.wants.length;++w){var t=k[d[w]];\"object\"===typeof t&&t.hasOwnProperty&&t.hasOwnProperty(\"delete\")&&t.delete()}v&&(a.C=v)}))}});a.i.attachMultiListener(f,h);f.delete()}\nD.onResults=function(a,b){this.listeners[b||\"$\"]=a};ya(\"Solution\",Nb);ya(\"OptionType\",{BOOL:0,NUMBER:1,aa:2,0:\"BOOL\",1:\"NUMBER\",2:\"STRING\"});function Wb(a){void 0===a&&(a=0);switch(a){case 1:return\"selfie_segmentation_landscape.tflite\";default:return\"selfie_segmentation.tflite\"}}\nfunction Xb(a){var b=this;a=a||{};this.g=new Nb({locateFile:a.locateFile,files:function(c){return[{simd:!0,url:\"selfie_segmentation_solution_simd_wasm_bin.js\"},{simd:!1,url:\"selfie_segmentation_solution_wasm_bin.js\"},{data:!0,url:Wb(c.modelSelection)}]},graph:{url:\"selfie_segmentation.binarypb\"},listeners:[{wants:[\"segmentation_mask\",\"image_transformed\"],outs:{image:{type:\"texture\",stream:\"image_transformed\"},segmentationMask:{type:\"texture\",stream:\"segmentation_mask\"}}}],inputs:{image:{type:\"video\",\nstream:\"input_frames_gpu\"}},options:{useCpuInference:{type:0,graphOptionXref:{calculatorType:\"InferenceCalculator\",fieldName:\"use_cpu_inference\"},default:\"iPad Simulator;iPhone Simulator;iPod Simulator;iPad;iPhone;iPod\".split(\";\").includes(navigator.platform)||navigator.userAgent.includes(\"Mac\")&&\"ontouchend\"in document},selfieMode:{type:0,graphOptionXref:{calculatorType:\"GlScalerCalculator\",calculatorIndex:1,fieldName:\"flip_horizontal\"}},modelSelection:{type:1,graphOptionXref:{calculatorType:\"ConstantSidePacketCalculator\",\ncalculatorName:\"ConstantSidePacketCalculatorModelSelection\",fieldName:\"int_value\"},onChange:function(c){return Y(b,function f(){var h,e,g=this,k;return O(f,function(l){if(1==l.g)return h=Wb(c),e=\"third_party/mediapipe/modules/selfie_segmentation/\"+h,N(l,Qb(g.g,h),2);k=l.h;g.g.overrideFile(e,k);return l.return(!0)})})}}}})}D=Xb.prototype;D.close=function(){this.g.close();return Promise.resolve()};D.onResults=function(a){this.g.onResults(a)};\nD.initialize=function(){return Y(this,function b(){var c=this;return O(b,function(d){return N(d,c.g.initialize(),0)})})};D.reset=function(){this.g.reset()};D.send=function(a){return Y(this,function c(){var d=this;return O(c,function(f){return N(f,d.g.send(a),0)})})};D.setOptions=function(a){this.g.setOptions(a)};ya(\"SelfieSegmentation\",Xb);ya(\"VERSION\",\"0.1.1632777926\");}).call(this);", "import { escape } from \"@web/core/utils/strings\";\n\n/**\n * Adds a span with a CSS class around chains of emojis in the message for styling purposes.\n * The input is first passed through 'escape' to prevent unwanted injections into the HTML\n *\n * Sequences of emojis are wrapped instead of individual ones to prevent compound emojis\n * such as \ud83d\udc69\ud83c\udfff = \ud83d\udc69 + \ud83c\udfff [dark skin tone character] from being separated.\n *\n * This will only match characters that have a different presentation from normal text, unlike \u00ae\n * For alternatives, see: https://www.unicode.org/reports/tr51/#Emoji_Properties_and_Data_Files\n *\n * @param {String} message a text message to format\n */\nexport function formatText(message) {\n    message = escape(message);\n    message = message.replaceAll(\n        /(\\p{Emoji_Presentation}+)/gu,\n        \"<span class='o_mail_emoji'>$1</span>\"\n    );\n    message = message.replace(/(?:\\r\\n|\\r|\\n)/g, \"<br>\");\n\n    return message;\n}\n", "import { useEffect } from \"@odoo/owl\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { useDebounced } from \"@web/core/utils/timing\";\nimport { charField, CharField } from \"@web/views/fields/char/char_field\";\nimport { textField, TextField } from \"@web/views/fields/text/text_field\";\n\n/**\n * Support a key-based onchange in text fields.\n * The triggerOnChange method is debounced to run after given debounce delay\n * (or 2 seconds by default) when typing ends.\n *\n */\nconst onchangeOnKeydownMixin = () => ({\n    setup() {\n        super.setup(...arguments);\n\n        if (this.props.onchangeOnKeydown) {\n            const input = this.input || this.textareaRef;\n\n            const triggerOnChange = useDebounced(\n                this.triggerOnChange,\n                this.props.keydownDebounceDelay\n            );\n            useEffect(() => {\n                if (input.el) {\n                    input.el.addEventListener(\"keydown\", triggerOnChange);\n                    return () => {\n                        input.el.removeEventListener(\"keydown\", triggerOnChange);\n                    };\n                }\n            });\n        }\n    },\n\n    triggerOnChange() {\n        const input = this.input || this.textareaRef;\n        input.el.dispatchEvent(new Event(\"change\"));\n    },\n});\n\npatch(CharField.prototype, onchangeOnKeydownMixin());\npatch(TextField.prototype, onchangeOnKeydownMixin());\n\nCharField.props = {\n    ...CharField.props,\n    onchangeOnKeydown: { type: Boolean, optional: true },\n    keydownDebounceDelay: { type: Number, optional: true },\n};\n\nTextField.props = {\n    ...TextField.props,\n    onchangeOnKeydown: { type: Boolean, optional: true },\n    keydownDebounceDelay: { type: Number, optional: true },\n};\n\nconst charExtractProps = charField.extractProps;\ncharField.extractProps = (fieldInfo) => {\n    return Object.assign(charExtractProps(fieldInfo), {\n        onchangeOnKeydown: exprToBoolean(fieldInfo.attrs.onchange_on_keydown),\n        keydownDebounceDelay: fieldInfo.attrs.keydown_debounce_delay\n            ? Number(fieldInfo.attrs.keydown_debounce_delay)\n            : 2000,\n    });\n};\n\nconst textExtractProps = textField.extractProps;\ntextField.extractProps = (fieldInfo) => {\n    return Object.assign(textExtractProps(fieldInfo), {\n        onchangeOnKeydown: exprToBoolean(fieldInfo.attrs.onchange_on_keydown),\n        keydownDebounceDelay: fieldInfo.attrs.keydown_debounce_delay\n            ? Number(fieldInfo.attrs.keydown_debounce_delay)\n            : 2000,\n    });\n};\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nexport function manageMessages({ component, env }) {\n    const resId = component.model.root.resId;\n    if (!resId) {\n        return null; // No record\n    }\n    const description = _t(\"Messages\");\n    return {\n        type: \"item\",\n        description,\n        callback: () => {\n            env.services.action.doAction({\n                res_model: \"mail.message\",\n                name: description,\n                views: [\n                    [false, \"list\"],\n                    [false, \"form\"],\n                ],\n                type: \"ir.actions.act_window\",\n                domain: [\n                    [\"res_id\", \"=\", resId],\n                    [\"model\", \"=\", component.props.resModel],\n                ],\n                context: {\n                    default_res_model: component.props.resModel,\n                    default_res_id: resId,\n                },\n            });\n        },\n        sequence: 130,\n        section: \"record\",\n    };\n}\n\nregistry.category(\"debug\").category(\"form\").add(\"mail.manageMessages\", manageMessages);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nimport { markup } from \"@odoo/owl\";\n\nregistry.category(\"web_tour.tours\").add(\"discuss_channel_tour\", {\n    url: \"/odoo\",\n    steps: () => [\n        {\n            isActive: [\"enterprise\"],\n            trigger: \"a[data-menu-xmlid='mail.menu_root_discuss']\",\n            content: _t(\"Open Discuss App\"),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o-mail-DiscussSidebarCategory-channel .o-mail-DiscussSidebarCategory-add\",\n            content: markup(\n                _t(\n                    \"<p>Channels make it easy to organize information across different topics and groups.</p> <p>Try to <b>create your first channel</b> (e.g. sales, marketing, product XYZ, after work party, etc).</p>\"\n                )\n            ),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o-discuss-ChannelSelector input\",\n            content: markup(_t(\"<p>Create a channel here.</p>\")),\n            tooltipPosition: \"bottom\",\n            run: `edit SomeChannel_${new Date().getTime()}`,\n        },\n        {\n            trigger: \".o-discuss-ChannelSelector-suggestion\",\n            content: markup(_t(\"<p>Create a public or private channel.</p>\")),\n            run: \"click\",\n            tooltipPosition: \"right\",\n        },\n        {\n            trigger: \".o-mail-Composer-input\",\n            content: markup(\n                _t(\n                    \"<p><b>Write a message</b> to the members of the channel here.</p> <p>You can notify someone with <i>'@'</i> or link another channel with <i>'#'</i>. Start your message with <i>'/'</i> to get the list of possible commands.</p>\"\n                )\n            ),\n            tooltipPosition: \"top\",\n            run: `edit SomeText_${new Date().getTime()}`,\n        },\n        {\n            trigger: \".o-mail-Composer-send:enabled\",\n            content: _t(\"Post your message on the thread\"),\n            tooltipPosition: \"top\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o-mail-Message:contains(today at)\",\n            content: _t(\"Hover on your message and mark as todo\"),\n            tooltipPosition: \"top\",\n            run: \"hover && click .o-mail-Message [title='Mark as Todo']\",\n        },\n        {\n            trigger: \"button:contains(Starred)\",\n            content: _t(\n                \"Once a message has been starred, you can come back and review it at any time here.\"\n            ),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o-mail-DiscussSidebarCategory-chat .o-mail-DiscussSidebarCategory-add\",\n            content: markup(\n                _t(\n                    \"<p><b>Chat with coworkers</b> in real-time using direct messages.</p><p><i>You might need to invite users from the Settings app first.</i></p>\"\n                )\n            ),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o-discuss-ChannelSelector\",\n        },\n    ],\n});\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: _t(\"Remove\"),\n                icon: \"fa fa-trash\",\n                onSelect: () => this.onClickUnlink(attachment),\n            });\n        }\n        if (this.canDownload(attachment)) {\n            res.push({\n                label: _t(\"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        if (this.message && !this.store.self.isInternalUser) {\n            return this.message.editable;\n        }\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\nexport function 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    onMounted,\n    onWillUnmount,\n    onWillUpdateProps,\n    useComponent,\n    useEffect,\n    useRef,\n    useState,\n} from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { deepEqual } from \"@web/core/utils/objects\";\nimport { hidePDFJSButtons } from \"@web/libs/pdfjs\";\n\nclass AbstractAttachmentView 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.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\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    get displayName() {\n        return this.state.thread.mainAttachment.filename;\n    }\n\n    onClickPopout() {}\n}\n\n/*\n * AttachmentView inside popout window.\n * Popout features disabled as this only makes sense in the non-popout AttachmentView.\n */\nexport class PopoutAttachmentView extends AbstractAttachmentView {\n    static template = \"mail.PopoutAttachmentView\";\n}\n\nexport function usePopoutAttachment() {\n    const component = useComponent();\n    const uiService = useService(\"ui\");\n    const mailPopoutService = useService(\"mail.popout\");\n\n    function 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    function showAttachmentView() {\n        const parentElementClassList = attachmentViewParentElementClassList();\n        const hiddenClass = \"d-none\";\n        if (parentElementClassList?.contains(hiddenClass)) {\n            parentElementClassList.remove(hiddenClass);\n        }\n    }\n\n    function hideAttachmentView() {\n        const parentElementClassList = attachmentViewParentElementClassList();\n        const hiddenClass = \"d-none\";\n        if (!parentElementClassList?.contains(hiddenClass)) {\n            parentElementClassList?.add(hiddenClass);\n        }\n    }\n\n    function extractPopoutProps(props) {\n        return {\n            threadId: props.threadId,\n            threadModel: props.threadModel,\n        };\n    }\n\n    function popout() {\n        mailPopoutService.addHooks(\n            () => {\n                hideAttachmentView();\n                uiService.bus.trigger(\"resize\");\n            },\n            () => {\n                showAttachmentView();\n                uiService.bus.trigger(\"resize\");\n            }\n        );\n        mailPopoutService.popout(PopoutAttachmentView, extractPopoutProps(component.props));\n    }\n\n    function updatePopout(newProps = component.props) {\n        if (mailPopoutService.externalWindow) {\n            hideAttachmentView();\n            mailPopoutService.popout(PopoutAttachmentView, extractPopoutProps(newProps));\n        }\n    }\n\n    function resetPopout() {\n        mailPopoutService.reset();\n    }\n\n    onMounted(updatePopout);\n    onWillUpdateProps((props) => {\n        const oldProps = extractPopoutProps(component.props);\n        const newProps = extractPopoutProps(props);\n        if (!deepEqual(oldProps, newProps)) {\n            updatePopout(newProps);\n        }\n    });\n    onWillUnmount(resetPopout);\n    return {\n        popout,\n        updatePopout,\n        resetPopout,\n    };\n}\n\n/**\n * @typedef {Object} Props\n * @property {number} threadId\n * @property {string} threadModel\n * @extends {Component<Props, Env>}\n */\nexport class AttachmentView extends AbstractAttachmentView {\n    setup() {\n        super.setup();\n        this.attachmentPopout = usePopoutAttachment();\n    }\n\n    onClickPopout() {\n        this.attachmentPopout.popout();\n    }\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            isFocused: false,\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                ev.stopPropagation();\n                this.state.value = this.props.value;\n                this.inputRef.el.blur();\n                break;\n        }\n    }\n\n    onBlurInput() {\n        this.state.isFocused = false;\n        this.props.onValidate(this.state.value);\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 { isMobileOS } from \"@web/core/browser/feature_detection\";\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.busMonitoring = useState(useService(\"bus.monitoring_service\"));\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    get isMobileOS() {\n        return isMobileOS();\n    }\n\n    onResize() {\n        this.chatHub.onRecompute();\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    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 true;\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\nexport const chatHubService = {\n    dependencies: [\"bus.monitoring_service\", \"mail.store\", \"ui\"],\n    start() {\n        registry.category(\"main_components\").add(\"mail.ChatHub\", { Component: ChatHub });\n    },\n};\nregistry.category(\"services\").add(\"mail.chat_hub\", chatHubService);\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 = 380; // same value as $o-mail-ChatWindow-width\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    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(available / (this.WINDOW + this.WINDOW_INBETWEEN))\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\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\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 hasActionsMenu() {\n        return (\n            this.partitionedActions.group.length > 0 ||\n            this.partitionedActions.other.length > 0 ||\n            (this.ui.isSmall && this.partitionedActions.quick.length > 2) ||\n            (!this.ui.isSmall && this.partitionedActions.quick.length > 3)\n        );\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.key === \"Escape\" && this.threadActions.activeAction) {\n            this.threadActions.activeAction.close();\n            ev.stopPropagation();\n            return;\n        }\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 (getActiveHotkey(ev)) {\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            case \"control+k\":\n                this.store.env.services.command.openMainPalette();\n                ev.preventDefault();\n                break;\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.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 { useCustomDropzone } from \"@web/core/dropzone/dropzone_hook\";\nimport { Picker, usePicker } from \"@mail/core/common/picker\";\nimport { MailAttachmentDropzone } from \"@mail/core/common/mail_attachment_dropzone\";\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    onWillUnmount,\n    useChildSubEnv,\n    useEffect,\n    useRef,\n    useState,\n    useExternalListener,\n    toRaw,\n    EventBus,\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            isFullComposerOpen: false,\n        });\n        this.fullComposerBus = new EventBus();\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            useCustomDropzone(\n                this.props.dropzoneRef,\n                MailAttachmentDropzone,\n                {\n                    extraClass: \"o-mail-Composer-dropzone\",\n                    onDrop: this.onDropFile,\n                },\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        onWillUnmount(() => {\n            this.props.composer.isFocused = false;\n        });\n        useEffect(\n            (composerThread, replyToThread) => {\n                if (replyToThread && replyToThread !== composerThread) {\n                    this.props.messageToReplyTo.cancel();\n                }\n            },\n            () => [this.props.composer.thread, this.props.messageToReplyTo?.thread]\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 && !this.props.composer.message;\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                if (this.isMobileOS) {\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        let default_body = await prettifyMessageContent(body, validMentions);\n        if (!default_body) {\n            const composer = toRaw(this.props.composer);\n            // Reset signature when recovering an empty body.\n            composer.emailAddSignature = true;\n        }\n        default_body = this.formatDefaultBodyForFullComposer(\n            default_body,\n            this.props.composer.emailAddSignature ? markup(this.store.self.signature) : \"\"\n        );\n        const context = {\n            default_attachment_ids: attachmentIds,\n            default_body,\n            default_email_add_signature: false,\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                    this.fullComposerBus.trigger(\"ACCIDENTAL_DISCARD\", {\n                        onAccidentalDiscard: (isEmpty) => {\n                            if (!isEmpty) {\n                                this.saveContent();\n                                this.restoreContent();\n                            }\n                        },\n                    });\n                } else {\n                    this.clear();\n                }\n                this.props.messageToReplyTo?.cancel();\n                this.onCloseFullComposerCallback();\n                this.state.isFullComposerOpen = false;\n                // Use another event bus so that no message is sent to the\n                // closed composer.\n                this.fullComposerBus = new EventBus();\n            },\n            props: {\n                fullComposerBus: this.fullComposerBus,\n            },\n        };\n        await this.env.services.action.doAction(action, options);\n        this.state.isFullComposerOpen = true;\n    }\n\n    formatDefaultBodyForFullComposer(defaultBody, signature = \"\") {\n        if (signature) {\n            defaultBody = `${defaultBody}<br>${signature}`;\n        }\n        return `<div>${defaultBody}</div>`; // as to not wrap in <p> by html_sanitize\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            emailAddSignature: composer.emailAddSignature,\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        this.props.composer.emailAddSignature = true;\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 saveContentToLocalStorage = (text, emailAddSignature) => {\n            const config = {\n                emailAddSignature,\n                text,\n            };\n            browser.localStorage.setItem(composer.localId, JSON.stringify(config));\n        };\n        if (this.state.isFullComposerOpen) {\n            this.fullComposerBus.trigger(\"SAVE_CONTENT\", {\n                onSaveContent: saveContentToLocalStorage,\n            });\n        } else {\n            saveContentToLocalStorage(composer.text, true);\n        }\n    }\n\n    restoreContent() {\n        const composer = toRaw(this.props.composer);\n        try {\n            const config = JSON.parse(browser.localStorage.getItem(composer.localId));\n            if (config.text) {\n                composer.emailAddSignature = config.emailAddSignature;\n                composer.text = config.text;\n            }\n        } catch {\n            browser.localStorage.removeItem(composer.localId);\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    /** @type {boolean} */\n    emailAddSignature = true;\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.svg\";\n    }\n\n    get body() {\n        if (this.notifications.length === 1 && this.lastMessage?.thread) {\n            return _t(\"An error occurred when sending an email on \u201c%(record_name)s\u201d\", {\n                record_name: this.lastMessage.thread.name,\n            });\n        }\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.debouncedSetImStatus(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 { Component } from \"@odoo/owl\";\nimport { Dropzone } from \"@web/core/dropzone/dropzone\";\n\nexport class MailAttachmentDropzone extends Component {\n    static template = \"mail.MailAttachmentDropzone\";\n    static components = { Dropzone };\n    static props = Dropzone.props;\n}\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 { App } from \"@odoo/owl\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"@web/core/registry\";\nimport { getTemplate } from \"@web/core/templates\";\nimport { setElementContent } from \"@web/core/utils/html\";\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?.document) {\n                setElementContent(externalWindow.document.head, \"\");\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 afterPopoutClosed 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 external window is created.\n         * @param {Function} afterPopoutClosed: this function is called after the external window is closed.\n         */\n        function addHooks(beforePopout = () => {}, afterPopoutClosed = () => {}) {\n            beforeFn = beforePopout;\n            afterFn = afterPopoutClosed;\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                beforeFn();\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            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 { setElementContent } from \"@web/core/utils/html\";\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.textContent = `\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                    setElementContent(\n                        bodyEl,\n                        this.state.showTranslation\n                            ? this.message.translationValue\n                            : this.props.messageSearch?.highlight(this.message.body) ??\n                                  this.message.body\n                    );\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 border border-secondary\": 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                    thread: this.props.thread,\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 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        btnClass: \"text-danger\",\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        get btnClass() {\n            return typeof action.btnClass === \"function\"\n                ? action.btnClass(component)\n                : action.btnClass;\n        },\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 { createDocumentFragmentFromContent } from \"@mail/utils/common/html\";\n\nimport { toRaw } from \"@odoo/owl\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { stateToUrl } from \"@web/core/browser/router\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { user } from \"@web/core/user\";\nimport { setElementContent } from \"@web/core/utils/html\";\nimport { url } from \"@web/core/utils/urls\";\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 htmlBody = createDocumentFragmentFromContent(this.body);\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                // \".o-mail-Message-edited\" is the class added by the mail.thread in _message_update_content\n                // when the message is edited\n                createDocumentFragmentFromContent(this.body).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            setElementContent(div, 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            setElementContent(div, 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_MED);\n        if (dateDay === DateTime.now().toLocaleString(DateTime.DATE_MED)) {\n            dateDay = _t(\"Today\");\n        }\n        return dateDay;\n    }\n\n    get dateSimple() {\n        return this.datetime\n            .toLocaleString(DateTime.TIME_SIMPLE, {\n                locale: user.lang,\n            })\n            .replace(\"\u202f\", \" \"); // so that AM/PM are properly wrapped\n    }\n\n    get dateSimpleWithDay() {\n        const userLocale = { locale: user.lang };\n        if (this.datetime.hasSame(DateTime.now(), \"day\")) {\n            return _t(\"Today at %(time)s\", {\n                time: this.datetime.toLocaleString(DateTime.TIME_SIMPLE, userLocale),\n            });\n        }\n        if (this.datetime.hasSame(DateTime.now().minus({ day: 1 }), \"day\")) {\n            return _t(\"Yesterday at %(time)s\", {\n                time: this.datetime.toLocaleString(DateTime.TIME_SIMPLE, userLocale),\n            });\n        }\n        if (this.datetime?.year === DateTime.now().year) {\n            return this.datetime.toLocaleString(\n                { ...DateTime.DATETIME_MED, year: undefined },\n                userLocale\n            );\n        }\n        return this.datetime.toLocaleString({ ...DateTime.DATETIME_MED }, userLocale);\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                [\n                    \"\",\n                    \"<p></p>\",\n                    \"<p><br></p>\",\n                    \"<p><br/></p>\",\n                    \"<div></div>\",\n                    \"<div><br></div>\",\n                    \"<div><br/></div>\",\n                ].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_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?.can_react);\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        this.store.insert(\n            await this.store.env.services.orm.silent.call(\n                \"mail.message\",\n                \"toggle_message_starred\",\n                [[this.id]]\n            )\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.preview = useDropdownState();\n        this.hover = useHover([\"reactionButton\", \"reactionList*\"], {\n            onHover: () => (this.preview.isOpen = true),\n            onAway: () => (this.preview.isOpen = false),\n            stateObserver: () => [this.preview?.isOpen],\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    }\n\n    /** @param {import(\"models\").MessageReactions} reaction */\n    previewText(reaction) {\n        const { count, content: emoji } = reaction;\n        const personNames = reaction.personas.slice(0, 3).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.props.message.canAddReaction()) {\n            return;\n        }\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    onClickReactionList(reaction) {\n        this.preview.isOpen = false; // closes dropdown immediately as to not recover focus after dropdown closes\n        this.props.openReactionMenu(reaction);\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 { createDocumentFragmentFromContent } from \"@mail/utils/common/html\";\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 = createDocumentFragmentFromContent(target);\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        onDelete() {\n            this.delete();\n        },\n    });\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 {\n    isAndroidApp,\n    isDisplayStandalone,\n    isIOS,\n    isIosApp,\n} from \"@web/core/browser/feature_detection\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nasync function getIosPwaPermission() {\n    if (browser.location.protocol !== \"https:\") {\n        return \"denied\";\n    }\n    const registration = await browser.navigator.serviceWorker?.getRegistration();\n    return (await registration?.pushManager.permissionState()) ?? \"prompt\";\n}\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            if (isIOS() && isDisplayStandalone()) {\n                permission = { state: await getIosPwaPermission() };\n            } else if (isIOS()) {\n                permission = { state: \"denied\" };\n            } else {\n                permission = await browser.navigator?.permissions?.query({\n                    name: \"notifications\",\n                });\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 && !isIOS()) {\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        let icon = \"/mail/static/src/img/odoobot_transparent.png\";\n        if (!author) {\n            notificationTitle = _t(\"New message\");\n        } else {\n            icon = author.avatarUrl;\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            icon,\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, icon }) {\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, icon, { 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, icon, { sound = true } = {}) {\n        const notification = new Notification(title, {\n            body: message,\n            icon,\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\";\nimport { debounce } from \"@web/core/utils/timing\";\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    static new() {\n        const record = super.new(...arguments);\n        record.debouncedSetImStatus = debounce(\n            (newStatus) => record.updateImStatus(newStatus),\n            this.IM_STATUS_DEBOUNCE_DELAY\n        );\n        return record;\n    }\n    static IM_STATUS_DEBOUNCE_DELAY = 1000;\n\n    /** @type {string} */\n    avatar_128_access_token;\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    debouncedSetImStatus;\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 = Record.attr(null, {\n        onUpdate() {\n            if (this.eq(this.store.self) && this.im_status === \"offline\") {\n                this.store.env.services.im_status.updateBusPresence();\n            }\n        },\n    });\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    groups_id = Record.many(\"res.groups\", { inverse: \"personas\" });\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        const accessTokenParam = {};\n        if (!this.store.self.isInternalUser) {\n            accessTokenParam.access_token = this.avatar_128_access_token;\n        }\n        if (this.type === \"partner\") {\n            return imageUrl(\"res.partner\", this.id, \"avatar_128\", {\n                ...accessTokenParam,\n                unique: this.write_date,\n            });\n        }\n        if (this.type === \"guest\") {\n            return imageUrl(\"mail.guest\", this.id, \"avatar_128\", {\n                ...accessTokenParam,\n                unique: this.write_date,\n            });\n        }\n        if (this.userId) {\n            return imageUrl(\"res.users\", this.userId, \"avatar_128\", {\n                unique: this.write_date,\n            });\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    updateImStatus(newStatus) {\n        this.im_status = newStatus;\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            popoverClass: \"border-secondary\",\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 { Record } from \"@mail/core/common/record\";\n\nexport class ResGroups extends Record {\n    static _name = \"res.groups\";\n    static id = \"id\";\n    personas = Record.many(\"Persona\");\n}\n\nResGroups.register();\n", "import { Component, useExternalListener, useState } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { useAutofocus } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"@mail/core/common/thread_model\").Thread} thread\n * @property {function} [closeSearch]\n */\n\nexport class SearchMessageInput extends Component {\n    static template = \"mail.SearchMessageInput\";\n    static props = [\"closeSearch?\", \"messageSearch\", \"thread\"];\n\n    setup() {\n        super.setup();\n        this.state = useState({ searchTerm: \"\", searchedTerm: \"\" });\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    }\n\n    search() {\n        this.props.messageSearch.searchTerm = this.state.searchTerm;\n        this.props.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.props.messageSearch.clear();\n        this.props.closeSearch?.();\n    }\n\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", "import { Component } from \"@odoo/owl\";\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 {Object} [messaageSearch]\n * @property {function} [onClickJump]\n * @property {function} [loadMore]\n */\nexport class SearchMessageResult extends Component {\n    static template = \"mail.SearchMessageResult\";\n    static components = { MessageCardList };\n    static props = [\"thread\", \"messageSearch\", \"onClickJump?\"];\n\n    get MESSAGE_FOUND() {\n        if (this.props.messageSearch.messages.length === 0) {\n            return false;\n        }\n        return _t(\"%s messages found\", this.props.messageSearch.count);\n    }\n\n    onLoadMoreVisible() {\n        const before = this.props.messageSearch?.messages\n            ? Math.min(...this.props.messageSearch.messages.map((message) => message.id))\n            : false;\n        this.props.messageSearch.search(before);\n    }\n}\n", "import { Component, useState, onWillUpdateProps } from \"@odoo/owl\";\nimport { ActionPanel } from \"@mail/discuss/core/common/action_panel\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { SearchMessageInput } from \"@mail/core/common/search_message_input\";\nimport { SearchMessageResult } from \"@mail/core/common/search_message_result\";\nimport { useMessageSearch } from \"./message_search_hook\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"@mail/core/common/thread_model\").Thread} thread\n */\nexport class SearchMessagesPanel extends Component {\n    static template = \"mail.SearchMessagesPanel\";\n    static components = { ActionPanel, SearchMessageInput, SearchMessageResult };\n    static props = [\"thread\"];\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n        this.messageSearch = useMessageSearch(this.props.thread);\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 Message\");\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.02, path: \"/mail/static/src/audio/ptt_push_1\" },\n            \"push-to-talk-off\": {\n                defaultVolume: 0.02,\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\";\nimport { browser } from \"@web/core/browser/browser\";\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/core/common/res_groups_model\").ResGroups} */\n    [\"res.groups\"];\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    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    isNotificationPermissionDismissed = Record.attr(false, {\n        compute() {\n            return (\n                browser.localStorage.getItem(\"mail.user_setting.push_notification_dismissed\") ===\n                \"true\"\n            );\n        },\n        /** @this {import(\"models\").DiscussApp} */\n        onUpdate() {\n            if (this.isNotificationPermissionDismissed) {\n                browser.localStorage.setItem(\n                    \"mail.user_setting.push_notification_dismissed\",\n                    \"true\"\n                );\n            } else {\n                browser.localStorage.removeItem(\"mail.user_setting.push_notification_dismissed\");\n            }\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             *\n             * In each group, thread with most recent message comes first\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);\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        }\n        return false;\n    }\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 {\n            attachments,\n            cannedResponseIds,\n            emailAddSignature,\n            isNote,\n            mentionedChannels,\n            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            email_add_signature: emailAddSignature,\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\", \"im_status\", \"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 { status, useComponent, useEffect, useState } from \"@odoo/owl\";\nimport { ConnectionAbortedError } from \"@web/core/network/rpc\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useDebounced } from \"@web/core/utils/timing\";\n\nclass UseSuggestion {\n    constructor(comp) {\n        this.comp = comp;\n        this.fetchSuggestions = useDebounced(this.fetchSuggestions.bind(this), 250);\n        useEffect(\n            () => {\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                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.fetchSuggestions();\n            },\n            () => [this.search.delimiter, this.search.position, this.search.term]\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    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    async fetchSuggestions() {\n        let resetFetchingState = true;\n        try {\n            this.abortController?.abort();\n            this.abortController = new AbortController();\n            this.state.isFetching = true;\n            await this.suggestionService.fetchSuggestions(this.search, {\n                thread: this.thread,\n                abortSignal: this.abortController.signal,\n            });\n        } catch (e) {\n            this.lastFetchedSearch = null;\n            if (e instanceof ConnectionAbortedError) {\n                resetFetchingState = false;\n                return;\n            }\n            throw e;\n        } finally {\n            if (resetFetchingState) {\n                this.state.isFetching = false;\n            }\n        }\n        if (status(this.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 (!this.state.items?.suggestions.length) {\n            this.clearSearch();\n        }\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, abortSignal } = {}) {\n        const cleanedSearchTerm = cleanTerm(term);\n        switch (delimiter) {\n            case \"@\": {\n                await this.fetchPartners(cleanedSearchTerm, thread, { abortSignal });\n                break;\n            }\n            case \"#\":\n                await this.fetchThreads(cleanedSearchTerm, { abortSignal });\n                break;\n            case \":\":\n                await this.store.cannedReponses.fetch();\n                break;\n        }\n    }\n\n    /**\n     * Make an ORM call with a cancellable signal. Usefull to abort fetch\n     * requests from outside of the suggestion service.\n     *\n     * @param {String} model\n     * @param {String} method\n     * @param {Array} args\n     * @param {Object} kwargs\n     * @param {Object} options\n     * @param {AbortSignal} options.abortSignal\n     */\n    makeOrmCall(model, method, args, kwargs, { abortSignal } = {}) {\n        return new Promise((res, rej) => {\n            const req = this.orm.silent.call(model, method, args, kwargs);\n            const onAbort = () => {\n                try {\n                    req.abort();\n                } catch (e) {\n                    rej(e);\n                }\n            };\n            abortSignal?.addEventListener(\"abort\", onAbort);\n            req.then(res)\n                .catch(rej)\n                .finally(() => abortSignal?.removeEventListener(\"abort\", onAbort));\n        });\n    }\n    /**\n     * @param {string} term\n     * @param {import(\"models\").Thread} [thread]\n     */\n    async fetchPartners(term, thread, { abortSignal } = {}) {\n        const kwargs = { search: term };\n        if (thread?.model === \"discuss.channel\") {\n            kwargs.channel_id = thread.id;\n        }\n        const data = await this.makeOrmCall(\n            \"res.partner\",\n            thread?.model === \"discuss.channel\"\n                ? \"get_mention_suggestions_from_channel\"\n                : \"get_mention_suggestions\",\n            [],\n            kwargs,\n            { abortSignal }\n        );\n        this.store.insert(data);\n    }\n\n    /**\n     * @param {string} term\n     */\n    async fetchThreads(term, { abortSignal } = {}) {\n        const suggestedThreads = await this.makeOrmCall(\n            \"discuss.channel\",\n            \"get_mention_suggestions\",\n            [],\n            { search: term },\n            { abortSignal }\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) => cleanTerm(cannedResponse.source).includes(cleanedSearchTerm)\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\" &&\n                    (thread.parent_channel_id || thread).group_public_id));\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            if (thread.channel_type === \"channel\") {\n                const group = (thread.parent_channel_id || thread).group_public_id;\n                partners = new Set([...partners, ...(group?.personas ?? [])]);\n            }\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                    searchTerm: 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                this.computeJumpPresentPosition();\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    computeJumpPresentPosition() {\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\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.scrollingToHighlight &&\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(() => {\n            this.computeJumpPresentPosition();\n            applyScroll();\n        });\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        this.scrollingToHighlight = 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.props.chatWindow &&\n                component.props.chatWindow.thread &&\n                (component.env.services[\"im_livechat.livechat\"] ||\n                    !component.env.services.ui.isSmall)\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            if (action.condition === undefined) {\n                return true;\n            }\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) {\n                    if (keepPrevious) {\n                        component.threadActions.actionStack.push(\n                            component.threadActions.activeAction\n                        );\n                    } else {\n                        component.threadActions.activeAction.close();\n                    }\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 opacity-75\",\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 (\n                thread.state === \"folded\" ||\n                (thread.state === \"open\" &&\n                    this.store.env.services.ui.isSmall &&\n                    this.store.env.services[\"im_livechat.livechat\"])\n            ) {\n                const cw = this.store.ChatWindow?.insert({ thread });\n                thread.store.chatHub.folded.delete(cw);\n                thread.store.chatHub.folded.unshift(cw);\n            }\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        });\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    group_public_id = Record.one(\"res.groups\");\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    /** @type {boolean} */\n    can_react = true;\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 (\n            !this.selfMember?.hideUnreadBanner &&\n            this.selfMember?.localMessageUnreadCounter > 0 &&\n            this.firstUnreadMessage\n        );\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            return this.store.Message.insert(messages.reverse());\n        } catch (e) {\n            this.hasLoadingFailed = true;\n            throw e;\n        } finally {\n            this.isLoaded = true;\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        } else {\n            this.markAsRead();\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 { patch } from \"@web/core/utils/patch\";\nimport { ChatHub } from \"@mail/core/common/chat_hub\";\n\npatch(ChatHub.prototype, {\n    get isShown() {\n        return super.isShown && !this.store.discuss.isActive;\n    },\n});\n", "import { AutoresizeInput } from \"@mail/core/common/autoresize_input\";\nimport { Composer } from \"@mail/core/common/composer\";\nimport { CountryFlag } from \"@mail/core/common/country_flag\";\nimport { ImStatus } from \"@mail/core/common/im_status\";\nimport { Thread } from \"@mail/core/common/thread\";\nimport { useThreadActions } from \"@mail/core/common/thread_actions\";\nimport { ThreadIcon } from \"@mail/core/common/thread_icon\";\nimport { DiscussSidebar } from \"@mail/core/public_web/discuss_sidebar\";\nimport {\n    useMessageEdition,\n    useMessageHighlight,\n    useMessageToReplyTo,\n} from \"@mail/utils/common/hooks\";\n\nimport {\n    Component,\n    onMounted,\n    onWillUnmount,\n    useRef,\n    useState,\n    useExternalListener,\n    useEffect,\n    useSubEnv,\n} from \"@odoo/owl\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { FileUploader } from \"@web/views/fields/file_handler\";\nimport { MessagingMenu } from \"@mail/core/public_web/messaging_menu\";\n\nexport class Discuss extends Component {\n    static components = {\n        AutoresizeInput,\n        CountryFlag,\n        DiscussSidebar,\n        Thread,\n        ThreadIcon,\n        Composer,\n        FileUploader,\n        ImStatus,\n        MessagingMenu,\n    };\n    static props = {\n        hasSidebar: { type: Boolean, optional: true },\n    };\n    static defaultProps = { hasSidebar: true };\n    static template = \"mail.Discuss\";\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n        this.messageHighlight = useMessageHighlight();\n        this.messageEdition = useMessageEdition();\n        this.messageToReplyTo = useMessageToReplyTo();\n        this.contentRef = useRef(\"content\");\n        this.root = useRef(\"root\");\n        this.state = useState({ jumpThreadPresent: 0 });\n        this.orm = useService(\"orm\");\n        this.effect = useService(\"effect\");\n        this.ui = useState(useService(\"ui\"));\n        useSubEnv({\n            inDiscussApp: true,\n            messageHighlight: this.messageHighlight,\n        });\n        this.notification = useService(\"notification\");\n        this.threadActions = useThreadActions();\n        useExternalListener(\n            window,\n            \"keydown\",\n            (ev) => {\n                if (getActiveHotkey(ev) === \"escape\" && !this.thread?.composer?.isFocused) {\n                    if (this.thread?.composer) {\n                        this.thread.composer.autofocus++;\n                    }\n                }\n            },\n            { capture: true }\n        );\n        if (this.store.inPublicPage) {\n            useEffect(\n                (thread, isSmall) => {\n                    if (!thread) {\n                        return;\n                    }\n                    if (isSmall) {\n                        this.chatWindow = this.thread.openChatWindow();\n                    } else {\n                        this.chatWindow?.close();\n                    }\n                },\n                () => [this.thread, this.ui.isSmall]\n            );\n        }\n        onMounted(() => (this.store.discuss.isActive = true));\n        onWillUnmount(() => (this.store.discuss.isActive = false));\n        useEffect(\n            (memberListAction) => {\n                if (!memberListAction) {\n                    return;\n                }\n                if (this.store.discuss.isMemberPanelOpenByDefault) {\n                    if (!this.threadActions.activeAction) {\n                        memberListAction.open();\n                    } else if (this.threadActions.activeAction === memberListAction) {\n                        return; // no-op (already open)\n                    } else {\n                        this.store.discuss.isMemberPanelOpenByDefault = false;\n                    }\n                }\n            },\n            () => [this.threadActions.actions.find((a) => a.id === \"member-list\")]\n        );\n    }\n\n    get thread() {\n        return this.store.discuss.thread;\n    }\n\n    async onFileUploaded(file) {\n        await this.thread.notifyAvatarToServer(file.data);\n        this.notification.add(_t(\"The avatar has been updated!\"), { type: \"success\" });\n    }\n\n    async renameThread(name) {\n        await this.thread.rename(name);\n    }\n\n    async updateThreadDescription(description) {\n        const newDescription = description.trim();\n        if (!newDescription && !this.thread.description) {\n            return;\n        }\n        if (newDescription !== this.thread.description) {\n            await this.thread.notifyDescriptionToServer(newDescription);\n        }\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    }\n}\n", "import { compareDatetime } from \"@mail/utils/common/misc\";\nimport { Record } from \"@mail/core/common/record\";\nimport { browser } from \"@web/core/browser/browser\";\n\nexport class DiscussAppCategory extends Record {\n    static id = \"id\";\n    /** @returns {import(\"models\").DiscussAppCategory} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").DiscussAppCategory|import(\"models\").DiscussAppCategory[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n\n    /**\n     * @param {import(\"models\").Thread} t1\n     * @param {import(\"models\").Thread} t2\n     */\n    sortThreads(t1, t2) {\n        if (this.id === \"channels\") {\n            return String.prototype.localeCompare.call(t1.name, t2.name);\n        }\n        if (this.id === \"chats\") {\n            return compareDatetime(t2.lastInterestDt, t1.lastInterestDt) || t2.id - t1.id;\n        }\n    }\n\n    get isVisible() {\n        return (\n            !this.hideWhenEmpty ||\n            this.threads.some((thread) => thread.displayToSelf || thread.isLocallyPinned)\n        );\n    }\n\n    /** @type {string} */\n    extraClass;\n    /** @string */\n    icon;\n    /** @string */\n    id;\n    /** @type {string} */\n    name;\n    hideWhenEmpty = false;\n    canView = false;\n    canAdd = false;\n    app = Record.one(\"DiscussApp\", {\n        compute() {\n            return this.store.discuss;\n        },\n    });\n    _openLocally = false;\n    localStateKey = Record.attr(null, {\n        compute() {\n            if (this.saveStateToServer) {\n                return null;\n            }\n            return `discuss_sidebar_category_${this.id}_open`;\n        },\n        onUpdate() {\n            if (this.localStateKey) {\n                this._openLocally = JSON.parse(\n                    browser.localStorage.getItem(this.localStateKey) ?? \"true\"\n                );\n            }\n        },\n    });\n    /** @type {number} */\n    sequence;\n\n    get open() {\n        return this.saveStateToServer\n            ? this.store.settings[this.serverStateKey]\n            : this._openLocally;\n    }\n\n    get saveStateToServer() {\n        return this.serverStateKey && this.store.self?.isInternalUser;\n    }\n\n    set open(value) {\n        if (this.saveStateToServer) {\n            this.store.settings[this.serverStateKey] = value;\n            this.store.env.services.orm.call(\n                \"res.users.settings\",\n                \"set_res_users_settings\",\n                [[this.store.settings.id]],\n                {\n                    new_settings: {\n                        [this.serverStateKey]: value,\n                    },\n                }\n            );\n        } else {\n            this._openLocally = value;\n            browser.localStorage.setItem(this.localStateKey, value);\n        }\n    }\n\n    /** @type {string} */\n    serverStateKey;\n    /** @type {string} */\n    addTitle;\n    /** @type {string} */\n    addHotkey;\n    threads = Record.many(\"Thread\", {\n        sort(t1, t2) {\n            return this.sortThreads(t1, t2);\n        },\n        inverse: \"discussAppCategory\",\n    });\n}\n\nDiscussAppCategory.register();\n", "import { Record } from \"@mail/core/common/record\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class DiscussApp extends Record {\n    static new(data) {\n        /** @type {import(\"models\").DiscussApp} */\n        const res = super.new(data);\n        Object.assign(res, {\n            channels: {\n                extraClass: \"o-mail-DiscussSidebarCategory-channel\",\n                icon: \"fa fa-hashtag\",\n                id: \"channels\",\n                name: _t(\"Channels\"),\n                canView: true,\n                canAdd: true,\n                sequence: 10,\n                serverStateKey: \"is_discuss_sidebar_category_channel_open\",\n                addTitle: _t(\"Add or join a channel\"),\n                addHotkey: \"c\",\n            },\n            chats: {\n                extraClass: \"o-mail-DiscussSidebarCategory-chat\",\n                icon: \"fa fa-users\",\n                id: \"chats\",\n                name: _t(\"Direct messages\"),\n                canView: false,\n                canAdd: true,\n                sequence: 30,\n                serverStateKey: \"is_discuss_sidebar_category_chat_open\",\n                addTitle: _t(\"Start a conversation\"),\n                addHotkey: \"d\",\n            },\n        });\n        return res;\n    }\n    /** @returns {import(\"models\").DiscussApp} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {import(\"models\").DiscussApp|import(\"models\").DiscussApp[]} */\n    static insert(data) {\n        return super.insert(...arguments);\n    }\n\n    INSPECTOR_WIDTH = 300;\n    /** @type {'main'|'channel'|'chat'|'livechat'} */\n    activeTab = \"main\";\n    searchTerm = \"\";\n    isActive = false;\n    isMemberPanelOpenByDefault = Record.attr(true, {\n        compute() {\n            return (\n                browser.localStorage.getItem(\"mail.user_setting.no_members_default_open\") !== \"true\"\n            );\n        },\n        /** @this {import(\"models\").DiscussApp} */\n        onUpdate() {\n            if (this.isMemberPanelOpenByDefault) {\n                browser.localStorage.removeItem(\"mail.user_setting.no_members_default_open\");\n            } else {\n                browser.localStorage.setItem(\"mail.user_setting.no_members_default_open\", \"true\");\n            }\n        },\n    });\n    isSidebarCompact = Record.attr(false, {\n        compute() {\n            return (\n                browser.localStorage.getItem(\"mail.user_setting.discuss_sidebar_compact\") === \"true\"\n            );\n        },\n        /** @this {import(\"models\").DiscussApp} */\n        onUpdate() {\n            if (this.isSidebarCompact) {\n                browser.localStorage.setItem(\n                    \"mail.user_setting.discuss_sidebar_compact\",\n                    this.isSidebarCompact.toString()\n                );\n            } else {\n                browser.localStorage.removeItem(\"mail.user_setting.discuss_sidebar_compact\");\n            }\n        },\n    });\n    allCategories = Record.many(\"DiscussAppCategory\", {\n        inverse: \"app\",\n        sort: (c1, c2) =>\n            c1.sequence !== c2.sequence\n                ? c1.sequence - c2.sequence\n                : c1.name.localeCompare(c2.name),\n    });\n    thread = Record.one(\"Thread\");\n    channels = Record.one(\"DiscussAppCategory\");\n    chats = Record.one(\"DiscussAppCategory\");\n    hasRestoredThread = false;\n}\n\nDiscussApp.register();\n", "import { Discuss } from \"@mail/core/public_web/discuss\";\n\nimport { Component, onWillStart, onWillUpdateProps, useState } from \"@odoo/owl\";\n\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {Object} Props\n * @property {Object} action\n * @property {Object} action.context\n * @property {number} [action.context.active_id]\n * @property {Object} [action.params]\n * @property {number} [action.params.active_id]\n * @extends {Component<Props, Env>}\n */\nexport class DiscussClientAction extends Component {\n    static components = { Discuss };\n    static props = [\"*\"];\n    static template = \"mail.DiscussClientAction\";\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n        onWillStart(() => {\n            // bracket to avoid blocking rendering with restore promise\n            this.restoreDiscussThread(this.props);\n        });\n        onWillUpdateProps((nextProps) => {\n            // bracket to avoid blocking rendering with restore promise\n            this.restoreDiscussThread(nextProps);\n        });\n    }\n\n    getActiveId(props) {\n        return (\n            props.action.context.active_id ??\n            props.action.params?.active_id ??\n            this.store.Thread.localIdToActiveId(this.store.discuss.thread?.localId) ??\n            \"mail.box_inbox\"\n        );\n    }\n\n    /**\n     * @param {string} rawActiveId\n     */\n    parseActiveId(rawActiveId) {\n        const [model, id] = rawActiveId.split(\"_\");\n        if (model === \"mail.box\") {\n            return [\"mail.box\", id];\n        }\n        return [model, parseInt(id)];\n    }\n\n    /**\n     * Restore the discuss thread according to the active_id in the action if\n     * necessary.\n     *\n     * @param {Props} props\n     */\n    async restoreDiscussThread(props) {\n        const rawActiveId = this.getActiveId(props);\n        const [model, id] = this.parseActiveId(rawActiveId);\n        const activeThread = await this.store.Thread.getOrFetch({ model, id });\n        if (activeThread && activeThread.notEq(this.store.discuss.thread)) {\n            if (props.action?.params?.highlight_message_id) {\n                activeThread.highlightMessage = props.action.params.highlight_message_id;\n                delete props.action.params.highlight_message_id;\n            }\n            activeThread.setAsDiscussThread(false);\n        }\n        this.store.discuss.hasRestoredThread = true;\n    }\n}\n\nregistry.category(\"actions\").add(\"mail.action_discuss\", DiscussClientAction);\n", "import { Component, useState } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\n\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport const discussSidebarItemsRegistry = registry.category(\"mail.discuss_sidebar_items\");\n\n/**\n * @typedef {Object} Props\n * @extends {Component<Props, Env>}\n */\nexport class DiscussSidebar extends Component {\n    static template = \"mail.DiscussSidebar\";\n    static props = {};\n    static components = { Dropdown, DropdownItem };\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n    }\n\n    get discussSidebarItems() {\n        return discussSidebarItemsRegistry.getAll();\n    }\n}\n", "import { CountryFlag } from \"@mail/core/common/country_flag\";\nimport { ImStatus } from \"@mail/core/common/im_status\";\nimport { NotificationItem } from \"@mail/core/public_web/notification_item\";\nimport { useDiscussSystray } from \"@mail/utils/common/hooks\";\n\nimport { Component, useExternalListener, useRef, useState } from \"@odoo/owl\";\n\nimport { hasTouch } from \"@web/core/browser/feature_detection\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\n\nexport class MessagingMenu extends Component {\n    static components = { CountryFlag, Dropdown, NotificationItem, ImStatus };\n    static props = [];\n    static template = \"mail.MessagingMenu\";\n\n    setup() {\n        super.setup();\n        this.discussSystray = useDiscussSystray();\n        this.store = useState(useService(\"mail.store\"));\n        this.hasTouch = hasTouch;\n        this.ui = useState(useService(\"ui\"));\n        this.state = useState({\n            activeIndex: null,\n            adding: false,\n        });\n        this.dropdown = useDropdownState();\n        this.notificationList = useRef(\"notification-list\");\n\n        useExternalListener(window, \"keydown\", this.onKeydown, true);\n    }\n\n    onClickThread(isMarkAsRead, thread) {\n        if (!isMarkAsRead) {\n            this.openDiscussion(thread);\n            return;\n        }\n        this.markAsRead(thread);\n    }\n\n    markAsRead(thread) {\n        if (thread.needactionMessages.length > 0) {\n            thread.markAllMessagesAsRead();\n        }\n        if (thread.model === \"discuss.channel\") {\n            thread.markAsRead();\n        }\n    }\n\n    navigate(direction) {\n        if (this.notificationItems.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.notificationItems.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.notificationItems.length - 1) {\n                    this.navigate(\"first\");\n                    return;\n                }\n                break;\n            default:\n                return;\n        }\n        this.state.activeIndex = targetId;\n        this.notificationItems[targetId]?.scrollIntoView({ block: \"nearest\" });\n    }\n\n    onKeydown(ev) {\n        if (!this.dropdown.isOpen) {\n            return;\n        }\n        const hotkey = getActiveHotkey(ev);\n        switch (hotkey) {\n            case \"enter\":\n                if (this.state.activeIndex === null) {\n                    return;\n                }\n                this.notificationItems[this.state.activeIndex].click();\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        ev.preventDefault();\n        ev.stopPropagation();\n    }\n\n    get notificationItems() {\n        return this.notificationList.el?.children ?? [];\n    }\n\n    get threads() {\n        return this.store.menuThreads;\n    }\n\n    /**\n     * @type {{ id: string, icon: string, label: string }[]}\n     */\n    get tabs() {\n        return [\n            {\n                icon: \"fa fa-user\",\n                id: \"chat\",\n                label: _t(\"Chat\"),\n            },\n            {\n                icon: \"fa fa-users\",\n                id: \"channel\",\n                label: _t(\"Channel\"),\n            },\n        ];\n    }\n\n    openDiscussion(thread) {\n        thread.open({ fromMessagingMenu: true });\n        this.dropdown.close();\n    }\n\n    onClickNavTab(tabId) {\n        if (this.store.discuss.activeTab === tabId) {\n            return;\n        }\n        this.store.discuss.activeTab = tabId;\n        if (\n            this.store.discuss.activeTab === \"main\" &&\n            this.env.inDiscussApp &&\n            (!this.store.discuss.thread || this.store.discuss.thread.model !== \"mail.box\")\n        ) {\n            this.store.inbox.setAsDiscussThread();\n        }\n        if (this.store.discuss.activeTab !== \"main\") {\n            this.store.discuss.thread = undefined;\n        }\n    }\n}\n\nregistry\n    .category(\"systray\")\n    .add(\"mail.messaging_menu\", { Component: MessagingMenu }, { sequence: 25 });\n", "import { ImStatus } from \"@mail/core/common/im_status\";\nimport { isToday } from \"@mail/utils/common/dates\";\nimport { useHover } from \"@mail/utils/common/hooks\";\n\nimport { Component, useRef, useState } from \"@odoo/owl\";\n\nimport { ActionSwiper } from \"@web/core/action_swiper/action_swiper\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nconst { DateTime } = luxon;\n\nexport class NotificationItem extends Component {\n    static components = { ActionSwiper, ImStatus };\n    static props = [\n        \"body?\",\n        \"counter?\",\n        \"datetime?\",\n        \"first?\",\n        \"hasMarkAsReadButton?\",\n        \"iconSrc?\",\n        \"muted?\",\n        \"onClick\",\n        \"onSwipeLeft?\",\n        \"onSwipeRight?\",\n        \"slots?\",\n        \"isActive?\",\n        \"nameMaxLine?\",\n        \"textMaxLine?\",\n    ];\n    static defaultProps = {\n        counter: 0,\n        muted: 0,\n    };\n    static template = \"mail.NotificationItem\";\n\n    setup() {\n        super.setup();\n        this.isToday = isToday;\n        this.DateTime = DateTime;\n        this.ui = useState(useService(\"ui\"));\n        this.markAsReadRef = useRef(\"markAsRead\");\n        this.rootHover = useHover(\"root\");\n    }\n\n    get dateText() {\n        if (isToday(this.props.datetime)) {\n            return this.props.datetime?.toLocaleString(DateTime.TIME_SIMPLE);\n        }\n        if (this.props.datetime?.year === DateTime.now().year) {\n            return this.props.datetime?.toLocaleString({ month: \"short\", day: \"numeric\" });\n        }\n        return this.props.datetime?.toLocaleString(DateTime.DATE_MED);\n    }\n\n    onClick(ev) {\n        this.props.onClick(ev.target === this.markAsReadRef.el);\n    }\n\n    webkitLineClamp(maxLine) {\n        return `\n            display: -webkit-box;\n            overflow: hidden;\n            -webkit-box-orient: vertical;\n            -webkit-line-clamp: ${maxLine};\n        `;\n    }\n}\n", "import { OutOfFocusService, outOfFocusService } from \"@mail/core/common/out_of_focus_service\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(OutOfFocusService.prototype, {\n    setup(env, services) {\n        super.setup(env, services);\n        this.titleService = services.title;\n        this.counter = 0;\n        this.contributingMessageLocalIds = new Set();\n        env.bus.addEventListener(\"window_focus\", () => this.clearUnreadMessage());\n    },\n    clearUnreadMessage() {\n        this.counter = 0;\n        this.contributingMessageLocalIds.clear();\n        this.titleService.setCounters({ discuss: undefined });\n    },\n    notify(message) {\n        if (this.contributingMessageLocalIds.has(message.localId)) {\n            return;\n        }\n        this.contributingMessageLocalIds.add(message.localId);\n        this.counter++;\n        this.titleService.setCounters({ discuss: this.counter });\n        super.notify(...arguments);\n    },\n});\noutOfFocusService.dependencies = [...outOfFocusService.dependencies, \"title\"];\n", "import { Store, storeService } from \"@mail/core/common/store_service\";\nimport { Record } from \"@mail/core/common/record\";\nimport { router } from \"@web/core/browser/router\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Store.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.discuss = Record.one(\"DiscussApp\");\n        this.action_discuss_id;\n    },\n    onStarted() {\n        super.onStarted(...arguments);\n        this.discuss = { activeTab: \"main\" };\n        this.env.bus.addEventListener(\n            \"discuss.channel/new_message\",\n            ({ detail: { channel, message, silent } }) => {\n                if (this.env.services.ui.isSmall || message.isSelfAuthored || silent) {\n                    return;\n                }\n                channel.notifyMessageToUser(message);\n            }\n        );\n    },\n    getDiscussSidebarCategoryCounter(categoryId) {\n        return this.DiscussAppCategory.get({ id: categoryId }).threads.reduce((acc, channel) => {\n            if (categoryId === \"channels\") {\n                return channel.message_needaction_counter > 0 ? acc + 1 : acc;\n            } else {\n                return channel.selfMember?.message_unread_counter > 0 ? acc + 1 : acc;\n            }\n        }, 0);\n    },\n});\n\npatch(storeService, {\n    start(env, services) {\n        const store = super.start(...arguments);\n        const discussActionIds = [\"mail.action_discuss\", \"discuss\"];\n        if (store.action_discuss_id) {\n            discussActionIds.push(store.action_discuss_id);\n        }\n        store.discuss.isActive ||= discussActionIds.includes(router.current.action);\n        services.ui.bus.addEventListener(\"resize\", () => {\n            store.discuss.activeTab = \"main\";\n            if (services.ui.isSmall && store.discuss.thread?.channel_type) {\n                store.discuss.activeTab = store.discuss.thread.channel_type;\n            }\n        });\n        return store;\n    },\n});\n", "import { patch } from \"@web/core/utils/patch\";\nimport { Thread } from \"@mail/core/common/thread_model\";\nimport { Record } from \"@mail/core/common/record\";\nimport { router } from \"@web/core/browser/router\";\n\npatch(Thread.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.discussAppCategory = Record.one(\"DiscussAppCategory\", {\n            compute() {\n                return this._computeDiscussAppCategory();\n            },\n        });\n    },\n\n    _computeDiscussAppCategory() {\n        if ([\"group\", \"chat\"].includes(this.channel_type)) {\n            return this.store.discuss.chats;\n        }\n        if (this.channel_type === \"channel\" && !this.parent_channel_id) {\n            return this.store.discuss.channels;\n        }\n    },\n    /**\n     * Handle the notification of a new message based on the notification setting of the user.\n     * Thread on mute:\n     * 1. No longer see the unread status: the bold text disappears and the channel name fades out.\n     * 2. Without sound + need action counter.\n     * Thread Notification Type:\n     * All messages:All messages sound + need action counter\n     * Mentions:Only mention sounds + need action counter\n     * Nothing: No sound + need action counter\n     *\n     * @param {import(\"models\").Message} message\n     */\n    notifyMessageToUser(message) {\n        const channel_notifications =\n            this.custom_notifications || this.store.settings.channel_notifications;\n        if (\n            !this.mute_until_dt &&\n            !this.store.settings.mute_until_dt &&\n            (this.channel_type !== \"channel\" ||\n                (this.channel_type === \"channel\" &&\n                    (channel_notifications === \"all\" ||\n                        (channel_notifications === \"mentions\" &&\n                            message.recipients?.includes(this.store.self)))))\n        ) {\n            if (this.model === \"discuss.channel\") {\n                let chatWindow = this.store.ChatWindow.get({ thread: this });\n                if (!chatWindow) {\n                    chatWindow = this.store.ChatWindow.insert({ thread: this });\n                    if (\n                        this.autoOpenChatWindowOnNewMessage &&\n                        !this.store.discuss.isActive &&\n                        this.store.chatHub.opened.length < this.store.chatHub.maxOpened\n                    ) {\n                        chatWindow.open();\n                    } else {\n                        chatWindow.fold();\n                    }\n                }\n            }\n            this.store.env.services[\"mail.out_of_focus\"].notify(message, this);\n        }\n    },\n    get autoOpenChatWindowOnNewMessage() {\n        return false;\n    },\n    /** @param {boolean} pushState */\n    setAsDiscussThread(pushState) {\n        if (pushState === undefined) {\n            pushState = this.notEq(this.store.discuss.thread);\n        }\n        this.store.discuss.thread = this;\n        this.store.discuss.activeTab =\n            !this.store.env.services.ui.isSmall || this.model === \"mail.box\"\n                ? \"main\"\n                : [\"chat\", \"group\"].includes(this.channel_type)\n                ? \"chat\"\n                : \"channel\";\n        if (pushState) {\n            this.setActiveURL();\n        }\n        if (\n            this.store.env.services.ui.isSmall &&\n            this.model !== \"mail.box\" &&\n            !this.store.shouldDisplayWelcomeViewInitially\n        ) {\n            this.open();\n        }\n    },\n\n    setActiveURL() {\n        const activeId =\n            typeof this.id === \"string\" ? `mail.box_${this.id}` : `discuss.channel_${this.id}`;\n        router.pushState({ active_id: activeId });\n        if (\n            this.store.action_discuss_id &&\n            this.store.env.services.action?.currentController?.action.id ===\n                this.store.action_discuss_id\n        ) {\n            // Keep the action stack up to date (used by breadcrumbs).\n            this.store.env.services.action.currentController.action.context.active_id = activeId;\n        }\n    },\n    open(options) {\n        if (this.store.env.services.ui.isSmall) {\n            this.openChatWindow(options);\n            return;\n        }\n        this.setAsDiscussThread();\n    },\n    async unpin() {\n        this.isLocallyPinned = false;\n        if (this.eq(this.store.discuss.thread)) {\n            router.replaceState({ active_id: undefined });\n        }\n        if (this.model === \"discuss.channel\" && this.is_pinned) {\n            return this.store.env.services.orm.silent.call(\n                \"discuss.channel\",\n                \"channel_pin\",\n                [this.id],\n                { pinned: false }\n            );\n        }\n    },\n});\n", "import { patch } from \"@web/core/utils/patch\";\nimport { Message } from \"@mail/core/common/message\";\nimport { onWillUnmount } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\n\npatch(Message.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.state.lastReadMoreIndex = 0;\n        this.state.isReadMoreByIndex = new Map();\n        onWillUnmount(() => {\n            this.messageBody.el?.querySelector(\".o-mail-read-more-less\")?.remove();\n        });\n    },\n\n    /**\n     * @override\n     * @param {HTMLElement} bodyEl\n     */\n    prepareMessageBody(bodyEl) {\n        super.prepareMessageBody(...arguments);\n        Array.from(bodyEl.querySelectorAll(\".o-mail-read-more-less\")).forEach((el) => el.remove());\n        this.insertReadMoreLess(bodyEl);\n    },\n\n    /**\n     * Modifies the message to add the 'read more/read less' functionality\n     * All element nodes with 'data-o-mail-quote' attribute are concerned.\n     * All text nodes after a ``#stopSpelling`` element are concerned.\n     * Those text nodes need to be wrapped in a span (toggle functionality).\n     * All consecutive elements are joined in one 'read more/read less'.\n     *\n     * @param {HTMLElement} bodyEl\n     */\n    insertReadMoreLess(bodyEl) {\n        /**\n         * @param {HTMLElement} e\n         * @param {string} selector\n         */\n        function prevAll(e, selector) {\n            const res = [];\n            while ((e = e.previousElementSibling)) {\n                if (e.matches(selector)) {\n                    res.push(e);\n                }\n            }\n            return res;\n        }\n\n        /**\n         * @param {HTMLElement} e\n         * @param {string} selector\n         */\n        function prev(e, selector) {\n            while ((e = e.previousElementSibling)) {\n                if (e.matches(selector)) {\n                    return e;\n                }\n            }\n        }\n\n        /** @param {HTMLElement} el */\n        function hide(el) {\n            el.dataset.oMailDisplay = el.style.display;\n            el.style.display = \"none\";\n        }\n\n        /**\n         * @param {HTMLElement} el\n         * @param {boolean} condition\n         */\n        function toggle(el, condition = false) {\n            if (condition) {\n                let newDisplay = el.dataset.oMailDisplay;\n                if (newDisplay === \"none\") {\n                    newDisplay = null;\n                }\n                el.style.display = newDisplay;\n            } else {\n                hide(el);\n            }\n        }\n\n        const groups = [];\n        let readMoreNodes;\n        const ELEMENT_NODE = 1;\n        const TEXT_NODE = 3;\n        /** @type {ChildNode[]} childrenEl */\n        const childrenEl = Array.from(bodyEl.childNodes).filter(\n            /** @param {ChildNode} childEl */\n            function (childEl) {\n                return (\n                    childEl.nodeType === ELEMENT_NODE ||\n                    (childEl.nodeType === TEXT_NODE && childEl.nodeValue.trim())\n                );\n            }\n        );\n        for (const childEl of childrenEl) {\n            // Hide Text nodes if \"stopSpelling\"\n            if (\n                childEl.nodeType === TEXT_NODE &&\n                prevAll(childEl, '[id*=\"stopSpelling\"]').length > 0\n            ) {\n                // Convert Text nodes to Element nodes\n                const newChildEl = document.createElement(\"span\");\n                newChildEl.textContent = childEl.textContent;\n                newChildEl.dataset.oMailQuote = \"1\";\n                childEl.parentNode.replaceChild(newChildEl, childEl);\n            }\n            // Create array for each 'read more' with nodes to toggle\n            if (\n                (childEl.nodeType === ELEMENT_NODE && childEl.getAttribute(\"data-o-mail-quote\")) ||\n                (childEl.nodeName === \"BR\" && prev(childEl, '[data-o-mail-quote=\"1\"]'))\n            ) {\n                if (!readMoreNodes) {\n                    readMoreNodes = [];\n                    groups.push(readMoreNodes);\n                }\n                hide(childEl);\n                readMoreNodes.push(childEl);\n            } else {\n                readMoreNodes = undefined;\n                this.insertReadMoreLess(childEl);\n            }\n        }\n\n        for (const group of groups) {\n            const index = this.state.lastReadMoreIndex++;\n            // Insert link just before the first node\n            const readMoreLessEl = document.createElement(\"a\");\n            readMoreLessEl.style.display = \"block\";\n            readMoreLessEl.className = \"o-mail-read-more-less\";\n            readMoreLessEl.href = \"#\";\n            readMoreLessEl.textContent = _t(\"Read More\");\n            group[0].parentNode.insertBefore(readMoreLessEl, group[0]);\n\n            // Toggle All next nodes\n            if (!this.state.isReadMoreByIndex.has(index)) {\n                this.state.isReadMoreByIndex.set(index, true);\n            }\n            const updateFromState = () => {\n                const isReadMore = this.state.isReadMoreByIndex.get(index);\n                for (const childEl of group) {\n                    hide(childEl);\n                    toggle(childEl, !isReadMore);\n                }\n                readMoreLessEl.textContent = isReadMore\n                    ? _t(\"Read More\").toString()\n                    : _t(\"Read Less\").toString();\n            };\n            readMoreLessEl.addEventListener(\"click\", (e) => {\n                e.preventDefault();\n                this.state.isReadMoreByIndex.set(index, !this.state.isReadMoreByIndex.get(index));\n                updateFromState();\n            });\n            updateFromState();\n        }\n    },\n});\n", "import { useAttachmentUploader } from \"@mail/core/common/attachment_uploader_hook\";\nimport { ActivityMailTemplate } from \"@mail/core/web/activity_mail_template\";\nimport { ActivityMarkAsDone } from \"@mail/core/web/activity_markasdone_popover\";\nimport { computeDelay, getMsToTomorrow } from \"@mail/utils/common/dates\";\nimport { AvatarCardPopover } from \"@mail/discuss/web/avatar_card/avatar_card_popover\";\n\nimport { Component, onMounted, onWillUnmount, useState } from \"@odoo/owl\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { FileUploader } from \"@web/views/fields/file_handler\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Activity} activity\n * @property {function} onActivityChanged\n * @property {function} reloadParentView\n * @extends {Component<Props, Env>}\n */\nexport class Activity extends Component {\n    static components = { ActivityMailTemplate, FileUploader };\n    static props = [\"activity\", \"onActivityChanged\", \"reloadParentView\"];\n    static template = \"mail.Activity\";\n\n    setup() {\n        super.setup();\n        this.storeService = useService(\"mail.store\");\n        this.state = useState({ showDetails: false });\n        this.markDonePopover = usePopover(ActivityMarkAsDone, { position: \"right\" });\n        this.avatarCard = usePopover(AvatarCardPopover);\n        onMounted(() => {\n            this.updateDelayAtNight();\n        });\n        onWillUnmount(() => browser.clearTimeout(this.updateDelayMidnightTimeout));\n        this.attachmentUploader = useAttachmentUploader(this.thread);\n    }\n\n    get displayName() {\n        if (this.props.activity.summary) {\n            return _t(\"\u201c%s\u201d\", this.props.activity.summary);\n        }\n        return this.props.activity.display_name;\n    }\n\n    updateDelayAtNight() {\n        browser.clearTimeout(this.updateDelayMidnightTimeout);\n        this.updateDelayMidnightTimeout = browser.setTimeout(\n            () => this.render(),\n            getMsToTomorrow() + 100\n        ); // Make sure there is no race condition\n    }\n\n    get delay() {\n        return computeDelay(this.props.activity.date_deadline);\n    }\n\n    toggleDetails() {\n        this.state.showDetails = !this.state.showDetails;\n    }\n\n    async onClickMarkAsDone(ev) {\n        if (this.markDonePopover.isOpen) {\n            this.markDonePopover.close();\n            return;\n        }\n        this.markDonePopover.open(ev.currentTarget, {\n            activity: this.props.activity,\n            hasHeader: true,\n            onActivityChanged: this.props.onActivityChanged,\n        });\n    }\n\n    async onFileUploaded(data) {\n        const thread = this.thread;\n        const { id: attachmentId } = await this.attachmentUploader.uploadData(data, {\n            activity: this.props.activity,\n        });\n        await this.props.activity.markAsDone([attachmentId]);\n        this.props.onActivityChanged(thread);\n        await thread.fetchNewMessages();\n    }\n\n    onClickAvatar(ev) {\n        const target = ev.currentTarget;\n        if (!this.avatarCard.isOpen) {\n            this.avatarCard.open(target, {\n                id: this.props.activity.persona.userId,\n            });\n        }\n    }\n\n    async edit() {\n        const thread = this.thread;\n        await this.props.activity.edit();\n        this.props.onActivityChanged(thread);\n    }\n\n    async unlink() {\n        const thread = this.thread;\n        this.props.activity.remove();\n        await this.env.services.orm.unlink(\"mail.activity\", [this.props.activity.id]);\n        this.props.onActivityChanged(thread);\n    }\n\n    get thread() {\n        return this.env.services[\"mail.store\"].Thread.insert({\n            model: this.props.activity.res_model,\n            id: this.props.activity.res_id,\n        });\n    }\n\n    /**\n     * @param {MouseEvent} ev\n     */\n    async onClick(ev) {\n        this.storeService.handleClickOnLink(ev, this.thread);\n    }\n}\n", "import { ActivityListPopover } from \"@mail/core/web/activity_list_popover\";\n\nimport { Component, useEnv, useRef } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\n\nexport class ActivityButton extends Component {\n    static props = {\n        record: { type: Object },\n    };\n    static template = \"mail.ActivityButton\";\n\n    setup() {\n        super.setup();\n        this.popover = usePopover(ActivityListPopover, { position: \"bottom-start\" });\n        this.buttonRef = useRef(\"button\");\n        this.env = useEnv();\n    }\n\n    get buttonClass() {\n        const classes = [];\n        switch (this.props.record.data.activity_state) {\n            case \"overdue\":\n                classes.push(\"text-danger\");\n                break;\n            case \"today\":\n                classes.push(\"text-warning\");\n                break;\n            case \"planned\":\n                classes.push(\"text-success\");\n                break;\n            default:\n                classes.push(\"text-muted\");\n                break;\n        }\n        switch (this.props.record.data.activity_exception_decoration) {\n            case \"warning\":\n                classes.push(\"text-warning\");\n                classes.push(this.props.record.data.activity_exception_icon);\n                break;\n            case \"danger\":\n                classes.push(\"text-danger\");\n                classes.push(this.props.record.data.activity_exception_icon);\n                break;\n            default: {\n                const { activity_ids, activity_type_icon } = this.props.record.data;\n                if (activity_ids.records.length) {\n                    classes.push(activity_type_icon || \"fa-tasks\");\n                    break;\n                }\n                classes.push(\"fa-clock-o btn-link text-dark\");\n                break;\n            }\n        }\n        return classes.join(\" \");\n    }\n\n    get title() {\n        if (this.props.record.data.activity_exception_decoration) {\n            return _t(\"Warning\");\n        }\n        if (this.props.record.data.activity_summary) {\n            return this.props.record.data.activity_summary;\n        }\n        if (this.props.record.data.activity_type_id) {\n            return this.props.record.data.activity_type_id[1 /* display_name */];\n        }\n        return _t(\"Show activities\");\n    }\n\n    async onClick() {\n        if (this.popover.isOpen) {\n            this.popover.close();\n        } else {\n            const resId = this.props.record.resId;\n            const selectedRecords = this.env?.model?.root?.selection ?? [];\n            const selectedIds = selectedRecords.map((r) => r.resId);\n            // If the current record is not selected, ignore the selection\n            const resIds =\n                selectedIds.includes(resId) && selectedIds.length > 1 ? selectedIds : undefined;\n            this.popover.open(this.buttonRef.el, {\n                activityIds: this.props.record.data.activity_ids.currentIds,\n                onActivityChanged: (thread) => {\n                    const recordToLoad = resIds ? selectedRecords : [this.props.record];\n                    recordToLoad.forEach((r) => r.load());\n                    this.popover.close();\n                },\n                resId,\n                resIds,\n                resModel: this.props.record.resModel,\n            });\n        }\n    }\n}\n", "import { ActivityListPopoverItem } from \"@mail/core/web/activity_list_popover_item\";\nimport { compareDatetime } from \"@mail/utils/common/misc\";\n\nimport { Component, onWillUpdateProps, useState } from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {Object} Props\n * @property {number[]} activityIds\n * @property {function} close\n * @property {number} [defaultActivityTypeId]\n * @property {function} onActivityChanged\n * @property {number} resId\n * @property {string} resModel\n * @extends {Component<Props, Env>}\n */\nexport class ActivityListPopover extends Component {\n    static components = { ActivityListPopoverItem };\n    static props = [\n        \"activityIds\",\n        \"close\",\n        \"defaultActivityTypeId?\",\n        \"onActivityChanged\",\n        \"resId\",\n        /** Ids of record selection used to schedule activities in batch; it must include resId. */\n        \"resIds?\",\n        \"resModel\",\n    ];\n    static template = \"mail.ActivityListPopover\";\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.store = useState(useService(\"mail.store\"));\n        this.updateFromProps(this.props);\n        onWillUpdateProps((props) => this.updateFromProps(props));\n    }\n\n    get activities() {\n        /** @type {import(\"models\").Activity[]} */\n        const allActivities = Object.values(this.store.Activity.records);\n        return allActivities\n            .filter((activity) => this.props.activityIds.includes(activity.id))\n            .sort((a, b) => compareDatetime(a.date_deadline, b.date_deadline) || a.id - b.id);\n    }\n\n    onClickAddActivityButton() {\n        this.store\n            .scheduleActivity(\n                this.props.resModel,\n                this.props.resIds ? this.props.resIds : [this.props.resId],\n                this.props.defaultActivityTypeId\n            )\n            .then(() => this.props.onActivityChanged());\n        this.props.close();\n    }\n\n    get doneActivities() {\n        return this.activities.filter((activity) => activity.state === \"done\");\n    }\n\n    get overdueActivities() {\n        return this.activities.filter((activity) => activity.state === \"overdue\");\n    }\n\n    get plannedActivities() {\n        return this.activities.filter((activity) => activity.state === \"planned\");\n    }\n\n    get todayActivities() {\n        return this.activities.filter((activity) => activity.state === \"today\");\n    }\n\n    async updateFromProps(props) {\n        const data = await this.orm.silent.call(\"mail.activity\", \"activity_format\", [\n            props.activityIds,\n        ]);\n        this.store.insert(data, { html: true });\n    }\n}\n", "import { useAttachmentUploader } from \"@mail/core/common/attachment_uploader_hook\";\nimport { ActivityMailTemplate } from \"@mail/core/web/activity_mail_template\";\nimport { ActivityMarkAsDone } from \"@mail/core/web/activity_markasdone_popover\";\nimport { computeDelay } from \"@mail/utils/common/dates\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { url } from \"@web/core/utils/urls\";\nimport { FileUploader } from \"@web/views/fields/file_handler\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Activity} activity\n * @property {function} [onActivityChanged]\n * @property {function} [onClickDoneAndScheduleNext]\n * @property {function} onClickEditActivityButton\n * @extends {Component<Props, Env>}\n */\nexport class ActivityListPopoverItem extends Component {\n    static components = { ActivityMailTemplate, ActivityMarkAsDone, FileUploader };\n    static props = [\n        \"activity\",\n        \"onActivityChanged?\",\n        \"onClickDoneAndScheduleNext?\",\n        \"onClickEditActivityButton?\",\n    ];\n    static template = \"mail.ActivityListPopoverItem\";\n\n    setup() {\n        super.setup();\n        this.state = useState({ hasMarkDoneView: false });\n        if (this.props.activity.activity_category === \"upload_file\") {\n            this.attachmentUploader = useAttachmentUploader(\n                this.env.services[\"mail.store\"].Thread.insert({\n                    model: this.props.activity.res_model,\n                    id: this.props.activity.res_id,\n                })\n            );\n        }\n        this.closeMarkAsDone = this.closeMarkAsDone.bind(this);\n    }\n\n    closeMarkAsDone() {\n        this.state.hasMarkDoneView = false;\n    }\n\n    get delayLabel() {\n        const diff = computeDelay(this.props.activity.date_deadline);\n        if (diff === 0) {\n            return _t(\"Today\");\n        } else if (diff === -1) {\n            return _t(\"Yesterday\");\n        } else if (diff < 0) {\n            return _t(\"%s days overdue\", Math.round(Math.abs(diff)));\n        } else if (diff === 1) {\n            return _t(\"Tomorrow\");\n        } else {\n            return _t(\"Due in %s days\", Math.round(Math.abs(diff)));\n        }\n    }\n\n    get hasCancelButton() {\n        const activity = this.props.activity;\n        return activity.state !== \"done\" && activity.can_write;\n    }\n\n    get hasEditButton() {\n        const activity = this.props.activity;\n        return activity.state !== \"done\" && activity.can_write;\n    }\n\n    get hasFileUploader() {\n        const activity = this.props.activity;\n        return activity.state !== \"done\" && activity.activity_category === \"upload_file\";\n    }\n\n    get hasMarkDoneButton() {\n        return this.props.activity.state !== \"done\" && !this.hasFileUploader;\n    }\n\n    onClickEditActivityButton() {\n        this.props.onClickEditActivityButton();\n        this.props.activity.edit().then(() => this.props.onActivityChanged?.());\n    }\n\n    onClickMarkAsDone() {\n        this.state.hasMarkDoneView = !this.state.hasMarkDoneView;\n    }\n\n    async onFileUploaded(data) {\n        const { id: attachmentId } = await this.attachmentUploader.uploadData(data, {\n            activity: this.props.activity,\n        });\n        await this.props.activity.markAsDone([attachmentId]);\n        this.props.onActivityChanged?.();\n    }\n\n    unlink() {\n        this.props.activity.remove();\n        this.env.services.orm\n            .unlink(\"mail.activity\", [this.props.activity.id])\n            .then(() => this.props.onActivityChanged?.());\n    }\n\n    get activityAssigneeAvatar() {\n        return url(\"/web/image\", {\n            field: \"avatar_128\",\n            id: this.props.activity.user_id[0],\n            model: \"res.users\",\n        });\n    }\n}\n", "import { Component, useState } from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Activity} activity\n * @property {function} [onClickButtons]\n * @property {function} [onActivityChanged]\n * @extends {Component<Props, Env>}\n */\nexport class ActivityMailTemplate extends Component {\n    static defaultProps = {\n        onClickButtons: () => {},\n    };\n    static props = [\"activity\", \"onClickButtons?\", \"onActivityChanged?\"];\n    static template = \"mail.ActivityMailTemplate\";\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n    }\n\n    /**\n     * @param {MouseEvent} ev\n     * @param {Object} mailTemplate\n     */\n    onClickPreview(ev, mailTemplate) {\n        ev.stopPropagation();\n        ev.preventDefault();\n        this.props.onClickButtons();\n        const action = {\n            name: _t(\"Compose Email\"),\n            type: \"ir.actions.act_window\",\n            res_model: \"mail.compose.message\",\n            views: [[false, \"form\"]],\n            target: \"new\",\n            context: {\n                default_res_ids: [this.props.activity.res_id],\n                default_model: this.props.activity.res_model,\n                default_subtype_xmlid: \"mail.mt_comment\",\n                default_template_id: mailTemplate.id,\n                force_email: true,\n            },\n        };\n        const thread = this.store.Thread.insert({\n            model: this.props.activity.res_model,\n            id: this.props.activity.res_id,\n        });\n        this.env.services.action.doAction(action, {\n            onClose: () => this.props.onActivityChanged?.(thread),\n        });\n    }\n\n    /**\n     * @param {MouseEvent} ev\n     * @param {Object} mailTemplate\n     */\n    async onClickSend(ev, mailTemplate) {\n        ev.stopPropagation();\n        ev.preventDefault();\n        this.props.onClickButtons();\n        const thread = this.store.Thread.insert({\n            model: this.props.activity.res_model,\n            id: this.props.activity.res_id,\n        });\n        await this.env.services.orm.call(this.props.activity.res_model, \"activity_send_mail\", [\n            [this.props.activity.res_id],\n            mailTemplate.id,\n        ]);\n        this.props.onActivityChanged?.(thread);\n    }\n}\n", "import { Component, onMounted, useExternalListener, useRef } from \"@odoo/owl\";\n\nexport class ActivityMarkAsDone extends Component {\n    static template = \"mail.ActivityMarkAsDone\";\n    static props = [\n        \"activity\",\n        \"close?\",\n        \"hasHeader?\",\n        \"onClickDoneAndScheduleNext?\",\n        \"onActivityChanged\",\n    ];\n    static defaultProps = {\n        hasHeader: false,\n    };\n\n    get isSuggested() {\n        return this.props.activity.chaining_type === \"suggest\";\n    }\n\n    setup() {\n        super.setup();\n        this.textArea = useRef(\"textarea\");\n        onMounted(() => {\n            this.textArea.el.focus();\n        });\n        useExternalListener(window, \"keydown\", this.onKeydown);\n    }\n\n    onKeydown(ev) {\n        if (ev.key === \"Escape\" && this.props.close) {\n            this.props.close();\n        }\n    }\n\n    async onClickDone() {\n        const { res_id, res_model } = this.props.activity;\n        const thread = this.env.services[\"mail.store\"].Thread.insert({\n            model: res_model,\n            id: res_id,\n        });\n        await this.props.activity.markAsDone();\n        this.props.onActivityChanged(thread);\n        await thread.fetchNewMessages();\n    }\n\n    async onClickDoneAndScheduleNext() {\n        const { res_id, res_model } = this.props.activity;\n        const thread = this.env.services[\"mail.store\"].Thread.insert({\n            model: res_model,\n            id: res_id,\n        });\n        if (this.props.onClickDoneAndScheduleNext) {\n            this.props.onClickDoneAndScheduleNext();\n        }\n        if (this.props.close) {\n            this.props.close();\n        }\n        const action = await this.props.activity.markAsDoneAndScheduleNext();\n        thread.fetchNewMessages();\n        this.props.onActivityChanged(thread);\n        if (!action) {\n            return;\n        }\n        await new Promise((resolve) => {\n            this.env.services.action.doAction(action, {\n                onClose: resolve,\n            });\n        });\n        this.props.onActivityChanged(thread);\n    }\n}\n", "import { Component, useState } from \"@odoo/owl\";\n\nimport { useDiscussSystray } from \"@mail/utils/common/hooks\";\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 { Domain } from \"@web/core/domain\";\nimport { user } from \"@web/core/user\";\n\nexport class ActivityMenu extends Component {\n    static components = { Dropdown };\n    static props = [];\n    static template = \"mail.ActivityMenu\";\n\n    setup() {\n        super.setup();\n        this.discussSystray = useDiscussSystray();\n        this.store = useState(useService(\"mail.store\"));\n        this.action = useService(\"action\");\n        this.userId = user.userId;\n        this.ui = useState(useService(\"ui\"));\n        this.dropdown = useDropdownState();\n    }\n\n    onBeforeOpen() {\n        this.store.fetchData({ systray_get_activities: true });\n    }\n\n    availableViews(group) {\n        return [\n            [false, \"kanban\"],\n            [false, \"list\"],\n            [false, \"form\"],\n            [false, \"activity\"],\n        ];\n    }\n\n    openActivityGroup(group, filter=\"all\") {\n        this.dropdown.close();\n        const context = {\n            // Necessary because activity_ids of mail.activity.mixin has auto_join\n            // So, duplicates are faking the count and \"Load more\" doesn't show up\n            force_search_count: 1,\n        };\n        if (group.model === \"mail.activity\") {\n            this.action.doAction(\"mail.mail_activity_without_access_action\", {\n                additionalContext: {\n                    active_ids: group.activity_ids,\n                    active_model: \"mail.activity\",\n                },\n            });\n            return;\n        }\n\n        if (filter === \"all\") {\n            context[\"search_default_activities_overdue\"] = 1;\n            context[\"search_default_activities_today\"] = 1;\n        }\n        else if (filter === \"overdue\") {\n            context[\"search_default_activities_overdue\"] = 1;\n        }\n        else if (filter === \"today\") {\n            context[\"search_default_activities_today\"] = 1;\n        }\n        else if (filter === \"upcoming_all\") {\n            context[\"search_default_activities_upcoming_all\"] = 1;\n        }\n\n        let domain = [[\"activity_user_id\", \"=\", this.userId]];\n        if (group.domain) {\n            domain = Domain.and([domain, group.domain]).toList();\n        }\n        const views = this.availableViews(group);\n\n        this.action.doAction(\n            {\n                context,\n                domain,\n                name: group.name,\n                res_model: group.model,\n                search_view_id: [false],\n                type: \"ir.actions.act_window\",\n                views,\n            },\n            {\n                clearBreadcrumbs: true,\n                viewType: group.view_type,\n            }\n        );\n    }\n\n    openMyActivities() {\n        this.dropdown.close();\n        this.action.doAction(\"mail.mail_activity_action_my\", { clearBreadcrumbs: true });\n    }\n}\n\nregistry\n    .category(\"systray\")\n    .add(\"mail.activity_menu\", { Component: ActivityMenu }, { sequence: 20 });\n", "import { Record } from \"@mail/core/common/record\";\nimport { assignDefined } from \"@mail/utils/common/misc\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { formatDate, formatDateTime } from \"@web/core/l10n/dates\";\n\n/**\n * @typedef Data\n * @property {string} activity_category\n * @property {[number, string]} activity_type_id\n * @property {string|false} activity_decoration\n * @property {boolean} can_write\n * @property {'suggest'|'trigger'} chaining_type\n * @property {string} create_date\n * @property {[number, string]} create_uid\n * @property {string} date_deadline\n * @property {string} date_done\n * @property {string} display_name\n * @property {boolean} has_recommended_activities\n * @property {string} icon\n * @property {number} id\n * @property {Object[]} mail_template_ids\n * @property {string} note\n * @property {number|false} previous_activity_type_id\n * @property {number|false} recommended_activity_type_id\n * @property {string} res_model\n * @property {[number, string]} res_model_id\n * @property {number} res_id\n * @property {string} res_name\n * @property {number|false} request_partner_id\n * @property {'overdue'|'planned'|'today'} state\n * @property {string} summary\n * @property {[number, string]} user_id\n * @property {string} write_date\n * @property {[number, string]} write_uid\n */\n\nexport class Activity extends Record {\n    static id = \"id\";\n    /** @type {Object.<number, import(\"models\").Activity>} */\n    static records = {};\n    /** @returns {import(\"models\").Activity} */\n    static get(data) {\n        return super.get(data);\n    }\n    /**\n     * @param {Data} data\n     * @param {Object} [param1]\n     * @param {boolean} param1.broadcast\n     * @returns {import(\"models\").Activity|import(\"models\").Activity[]}\n     */\n    static insert(data, { broadcast = true } = {}) {\n        return super.insert(...arguments);\n    }\n    /**\n     * @param {Data} data\n     * @param {Object} [param1]\n     * @param {boolean} param1.broadcast\n     * @returns {import(\"models\").Activity}\n     */\n    static _insert(data, { broadcast = true } = {}) {\n        /** @type {import(\"models\").Activity} */\n        const activity = this.preinsert(data);\n        if (data.request_partner_id) {\n            data.request_partner_id = data.request_partner_id[0];\n        }\n        assignDefined(activity, data);\n        if (broadcast) {\n            this.store.activityBroadcastChannel?.postMessage({\n                type: \"INSERT\",\n                payload: activity.serialize(),\n            });\n        }\n        return activity;\n    }\n\n    /** @type {boolean} */\n    active;\n    /** @type {string} */\n    activity_category;\n    /** @type {[number, string]} */\n    activity_type_id;\n    /** @type {string|false} */\n    activity_decoration;\n    /** @type {Object[]} */\n    attachment_ids;\n    /** @type {boolean} */\n    can_write;\n    /** @type {'suggest'|'trigger'} */\n    chaining_type;\n    /** @type {luxon.DateTime} */\n    create_date = Record.attr(undefined, { type: \"datetime\" });\n    /** @type {[number, string]} */\n    create_uid;\n    /** @type {luxon.DateTime} */\n    date_deadline = Record.attr(undefined, { type: \"date\" });\n    /** @type {luxon.DateTime} */\n    date_done = Record.attr(undefined, { type: \"date\" });\n    /** @type {string} */\n    display_name;\n    /** @type {boolean} */\n    has_recommended_activities;\n    /** @type {string} */\n    feedback;\n    /** @type {string} */\n    icon = \"fa-tasks\";\n    /** @type {number} */\n    id;\n    /** @type {Object[]} */\n    mail_template_ids;\n    note = Record.attr(\"\", { html: true });\n    persona = Record.one(\"Persona\");\n    /** @type {number|false} */\n    previous_activity_type_id;\n    /** @type {number|false} */\n    recommended_activity_type_id;\n    /** @type {string} */\n    res_model;\n    /** @type {[number, string]} */\n    res_model_id;\n    /** @type {number} */\n    res_id;\n    /** @type {string} */\n    res_name;\n    /** @type {number|false} */\n    request_partner_id;\n    /** @type {'overdue'|'planned'|'today'} */\n    state;\n    /** @type {string} */\n    summary;\n    /** @type {[number, string]} */\n    user_id;\n    /** @type {string} */\n    write_date;\n    /** @type {[number, string]} */\n    write_uid;\n\n    get dateDeadlineFormatted() {\n        return formatDate(this.date_deadline);\n    }\n\n    get dateDoneFormatted() {\n        return formatDate(this.date_done);\n    }\n\n    get dateCreateFormatted() {\n        return formatDateTime(this.create_date);\n    }\n\n    async edit() {\n        return new Promise((resolve) =>\n            this.store.env.services.action.doAction(\n                {\n                    type: \"ir.actions.act_window\",\n                    name: _t(\"Schedule Activity\"),\n                    res_model: \"mail.activity\",\n                    view_mode: \"form\",\n                    views: [[false, \"form\"]],\n                    target: \"new\",\n                    res_id: this.id,\n                    context: {\n                        default_res_model: this.res_model,\n                        default_res_id: this.res_id,\n                    },\n                },\n                { onClose: resolve }\n            )\n        );\n    }\n\n    /** @param {number[]} attachmentIds */\n    async markAsDone(attachmentIds = []) {\n        await this.store.env.services.orm.call(\"mail.activity\", \"action_feedback\", [[this.id]], {\n            attachment_ids: attachmentIds,\n            feedback: this.feedback,\n        });\n        this.store.activityBroadcastChannel?.postMessage({\n            type: \"RELOAD_CHATTER\",\n            payload: { id: this.res_id, model: this.res_model },\n        });\n    }\n\n    async markAsDoneAndScheduleNext() {\n        const action = await this.store.env.services.orm.call(\n            \"mail.activity\",\n            \"action_feedback_schedule_next\",\n            [[this.id]],\n            { feedback: this.feedback }\n        );\n        this.activityBroadcastChannel?.postMessage({\n            type: \"RELOAD_CHATTER\",\n            payload: { id: this.res_id, model: this.res_model },\n        });\n        return action;\n    }\n\n    remove({ broadcast = true } = {}) {\n        this.delete();\n        if (broadcast) {\n            this.activityBroadcastChannel?.postMessage({\n                type: \"DELETE\",\n                payload: { id: this.id },\n            });\n        }\n    }\n\n    serialize() {\n        return JSON.parse(JSON.stringify(this.toData()));\n    }\n}\n\nActivity.register();\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { escape } from \"@web/core/utils/strings\";\nimport { formatList } from \"@web/core/l10n/utils\";\nimport { markup, Component } from \"@odoo/owl\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\n\nimport { RecipientList } from \"@mail/core/web/recipient_list\";\nimport { SuggestedRecipientsList } from \"@mail/core/web/suggested_recipient_list\";\nimport { Thread } from \"@mail/core/common/thread_model\";\n\n\nexport class BaseRecipientsList extends Component {\n    static template = \"mail.BaseRecipientsList\";\n    static components = { SuggestedRecipientsList };\n    static props = { thread: { type: Thread } };\n\n    setup() {\n        this.recipientsPopover = usePopover(RecipientList);\n    }\n\n    /** @returns {Markup} */\n    getRecipientListToHTML() {\n        const recipients = this.props.thread.recipients.slice(0, 5).map((\n            { partner }) => {\n                const text = partner.email ? partner.emailWithoutDomain : partner.name;\n                return `<span class=\"text-muted\" title=\"${escape(\n                    partner.email || _t(\"no email address\")\n                )}\">${escape(text)}</span>`;\n            });\n        if (this.props.thread.recipients.length > 5) {\n            recipients.push(escape(\n                _t(\"%(recipientCount)s more\", {\n                    recipientCount: this.props.thread.recipients.length - 5}))\n            );\n        }\n        return markup(formatList(recipients));\n    }\n\n    /** @param {Event} ev */\n    onClickRecipientList(ev) {\n        if (this.recipientsPopover.isOpen) {\n            return this.recipientsPopover.close();\n        }\n        this.recipientsPopover.open(ev.target, {\n            thread: this.props.thread\n        });\n    }\n};\n", "import { ChatWindow } from \"@mail/core/common/chat_window_model\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(ChatWindow.prototype, {\n    async _onClose(options) {\n        if (\n            this.store.env.services.ui.isSmall &&\n            !this.store.discuss.isActive &&\n            this.fromMessagingMenu\n        ) {\n            // If we are in mobile and discuss is not open, it means the\n            // chat window was opened from the messaging menu. In that\n            // case it should be re-opened to simulate it was always\n            // there in the background.\n            document.querySelector(\".o_menu_systray i[aria-label='Messages']\")?.click();\n            // ensure messaging menu is opened before chat window is closed\n            await Promise.resolve();\n        }\n        await super._onClose(options);\n    },\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nconst commandCategoryRegistry = registry.category(\"command_categories\");\n\ncommandCategoryRegistry\n    .add(\"discuss_mentioned\", { namespace: \"@\", name: _t(\"Mentions\") }, { sequence: 10 })\n    .add(\"discuss_recent\", { namespace: \"#\", name: _t(\"Recent\") }, { sequence: 10 });\n", "import { SIGNATURE_CLASS } from \"@html_editor/main/signature_plugin\";\nimport { wrapInlinesInBlocks } from \"@html_editor/utils/dom\";\nimport { childNodes } from \"@html_editor/utils/dom_traversal\";\nimport { parseHTML } from \"@html_editor/utils/html\";\nimport { Composer } from \"@mail/core/common/composer\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { renderToElement } from \"@web/core/utils/render\";\n\npatch(Composer.prototype, {\n    /**\n     * Construct an editor friendly html representation of the body.\n     *\n     * @param {string} defaultBody\n     * @param {Markup} signature\n     * @returns {string}\n     */\n    formatDefaultBodyForFullComposer(defaultBody, signature = \"\") {\n        const fragment = parseHTML(document, defaultBody);\n        if (!fragment.firstChild) {\n            fragment.append(document.createElement(\"BR\"));\n        }\n        if (signature) {\n            const signatureEl = renderToElement(\"html_editor.Signature\", {\n                signature,\n                signatureClass: SIGNATURE_CLASS,\n            });\n            fragment.append(signatureEl);\n        }\n        const container = document.createElement(\"DIV\");\n        container.append(...childNodes(fragment));\n        wrapInlinesInBlocks(container, { baseContainerNodeName: \"DIV\" });\n        return container.innerHTML;\n    },\n});\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Dialog.prototype, {\n    /**\n     * @override\n     */\n    onEscape() {\n        if (this.data.model === \"mail.compose.message\") {\n            return;\n        }\n        super.onEscape();\n    },\n});\n", "import { useEffect } from \"@odoo/owl\";\n\nimport { Discuss } from \"@mail/core/public_web/discuss\";\nimport { MessagingMenu } from \"@mail/core/public_web/messaging_menu\";\n\nimport { ControlPanel } from \"@web/search/control_panel/control_panel\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\n\nObject.assign(Discuss.components, { ControlPanel, MessagingMenu });\n\npatch(Discuss.prototype, {\n    setup() {\n        super.setup();\n        this.prevInboxCounter = this.store.inbox.counter;\n        useEffect(\n            (threadName) => {\n                if (threadName) {\n                    this.env.config?.setDisplayName(threadName);\n                }\n            },\n            () => [this.thread?.displayName]\n        );\n        useEffect(\n            () => {\n                if (\n                    this.thread?.id === \"inbox\" &&\n                    this.prevInboxCounter !== this.store.inbox.counter &&\n                    this.store.inbox.counter === 0\n                ) {\n                    this.effect.add({\n                        message: _t(\"Congratulations, your inbox is empty!\"),\n                        type: \"rainbow_man\",\n                        fadeout: \"fast\",\n                    });\n                }\n                this.prevInboxCounter = this.store.inbox.counter;\n            },\n            () => [this.store.inbox.counter]\n        );\n    },\n});\n", "import { ThreadIcon } from \"@mail/core/common/thread_icon\";\nimport { discussSidebarItemsRegistry } from \"@mail/core/public_web/discuss_sidebar\";\nimport { useHover } from \"@mail/utils/common/hooks\";\n\nimport { Component, useRef, useState } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { markEventHandled } from \"@web/core/utils/misc\";\n\nexport class Mailbox extends Component {\n    static template = \"mail.Mailbox\";\n    static props = [\"mailbox\"];\n    static components = { Dropdown, ThreadIcon };\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n        this.hover = useHover([\"root\", \"floating*\"], {\n            onHover: () => (this.floating.isOpen = true),\n            onAway: () => (this.floating.isOpen = false),\n        });\n        this.floating = useDropdownState();\n        this.rootRef = useRef(\"root\");\n    }\n\n    /** @returns {import(\"models\").Thread} */\n    get mailbox() {\n        return this.props.mailbox;\n    }\n\n    /** @param {MouseEvent} ev */\n    openThread(ev) {\n        markEventHandled(ev, \"sidebar.openThread\");\n        this.mailbox.setAsDiscussThread();\n    }\n}\n\n/**\n * @typedef {Object} Props\n * @extends {Component<Props, Env>}\n */\nexport class DiscussSidebarMailboxes extends Component {\n    static template = \"mail.DiscussSidebarMailboxes\";\n    static props = {};\n    static components = { Mailbox };\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n    }\n}\n\ndiscussSidebarItemsRegistry.add(\"mailbox\", DiscussSidebarMailboxes, { sequence: 20 });\n", "import { patch } from \"@web/core/utils/patch\";\nimport { DiscussSidebar } from \"../public_web/discuss_sidebar\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useState } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useHover } from \"@mail/utils/common/hooks\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\n\npatch(DiscussSidebar.prototype, {\n    setup() {\n        super.setup();\n        this.ui = useState(useService(\"ui\"));\n        this.meetingHover = useHover([\"meeting-btn\", \"meeting-floating*\"], {\n            onHover: () => (this.meetingFloating.isOpen = true),\n            onAway: () => (this.meetingFloating.isOpen = false),\n        });\n        this.meetingFloating = useDropdownState();\n    },\n    get startMeetingText() {\n        return _t(\"Start a meeting\");\n    },\n});\n", "import { Component, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { FollowerSubtypeDialog } from \"./follower_subtype_dialog\";\nimport { useVisible } from \"@mail/utils/common/hooks\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\n\n/**\n * @typedef {Object} Props\n * @property {function} [onAddFollowers]\n * @property {function} [onFollowerChanged]\n * @property {import('@mail/core/common/thread_model').Thread} thread\n * @extends {Component<Props, Env>}\n */\n\nexport class FollowerList extends Component {\n    static template = \"mail.FollowerList\";\n    static components = { DropdownItem };\n    static props = [\"onAddFollowers?\", \"onFollowerChanged?\", \"thread\", \"dropdown\"];\n\n    setup() {\n        super.setup();\n        this.action = useService(\"action\");\n        this.store = useState(useService(\"mail.store\"));\n        useVisible(\"load-more\", (isVisible) => {\n            if (isVisible) {\n                this.props.thread.loadMoreFollowers();\n            }\n        });\n    }\n\n    onClickAddFollowers() {\n        const action = {\n            type: \"ir.actions.act_window\",\n            res_model: \"mail.wizard.invite\",\n            view_mode: \"form\",\n            views: [[false, \"form\"]],\n            name: _t(\"Add followers to this document\"),\n            target: \"new\",\n            context: {\n                default_res_model: this.props.thread.model,\n                default_res_id: this.props.thread.id,\n                dialog_size: \"medium\",\n            },\n        };\n        this.action.doAction(action, {\n            onClose: () => {\n                this.props.onAddFollowers?.();\n            },\n        });\n    }\n\n    /**\n     * @param {MouseEvent} ev\n     * @param {import(\"models\").Follower} follower\n     */\n    onClickDetails(ev, follower) {\n        this.store.openDocument({ id: follower.partner.id, model: \"res.partner\" });\n        this.props.dropdown.close();\n    }\n\n    /**\n     * @param {MouseEvent} ev\n     * @param {import(\"models\").Follower} follower\n     */\n    async onClickEdit(ev, follower) {\n        this.env.services.dialog.add(FollowerSubtypeDialog, {\n            follower,\n            onFollowerChanged: () => this.props.onFollowerChanged?.(this.props.thread),\n        });\n        this.props.dropdown.close();\n    }\n\n    /**\n     * @param {MouseEvent} ev\n     * @param {import(\"models\").Follower} follower\n     */\n    async onClickRemove(ev, follower) {\n        const thread = this.props.thread;\n        await follower.remove();\n        this.props.onFollowerChanged?.(thread);\n    }\n}\n", "import { rpc } from \"@web/core/network/rpc\";\nimport { Component, onWillStart, useState } from \"@odoo/owl\";\n\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {Object} SubtypeData\n * @property {boolean} followed\n * @property {number} id\n * @property {string} name\n */\n\n/**\n * @typedef {Object} Props\n * @property {function} close\n * @property {import(\"models\").Follower} follower\n * @property {function} onFollowerChanged\n * @extends {Component<Props, Env>}\n */\nexport class FollowerSubtypeDialog extends Component {\n    static components = { Dialog };\n    static props = [\"close\", \"follower\", \"onFollowerChanged\"];\n    static template = \"mail.FollowerSubtypeDialog\";\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n        this.state = useState({\n            /** @type {SubtypeData[]} */\n            subtypes: [],\n        });\n        onWillStart(async () => {\n            this.state.subtypes = await rpc(\"/mail/read_subscription_data\", {\n                follower_id: this.props.follower.id,\n            });\n        });\n    }\n\n    /**\n     * @param {Event} ev\n     * @param {SubtypeData} subtype\n     */\n    onChangeCheckbox(ev, subtype) {\n        subtype.followed = ev.target.checked;\n    }\n\n    async onClickApply() {\n        const selectedSubtypes = this.state.subtypes.filter((s) => s.followed);\n        const thread = this.props.follower.thread;\n        if (selectedSubtypes.length === 0) {\n            await this.props.follower.remove();\n        } else {\n            await this.env.services.orm.call(\n                this.props.follower.thread.model,\n                \"message_subscribe\",\n                [[this.props.follower.thread.id]],\n                {\n                    partner_ids: [this.props.follower.partner.id],\n                    subtype_ids: selectedSubtypes.map((subtype) => subtype.id),\n                }\n            );\n            if (!selectedSubtypes.some((subtype) => subtype.id === this.store.mt_comment_id)) {\n                this.props.follower.removeRecipient();\n            }\n            this.env.services.notification.add(\n                _t(\"The subscription preferences were successfully applied.\"),\n                { type: \"success\" }\n            );\n        }\n        this.props.onFollowerChanged(thread);\n        this.props.close();\n    }\n\n    get title() {\n        return _t(\"Edit Subscription of %(name)s\", { name: this.props.follower.partner.name });\n    }\n}\n", "import { ColumnProgress } from \"@web/views/view_components/column_progress\";\n\nexport class MailColumnProgress extends ColumnProgress {\n    static props = {\n        ...ColumnProgress.props,\n        aggregateOn: { type: Object, optional: true },\n    };\n    static template = \"mail.ColumnProgress\";\n}\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport {\n    many2ManyBinaryField,\n    Many2ManyBinaryField\n} from \"@web/views/fields/many2many_binary/many2many_binary_field\";\n\nexport class MailComposerAttachmentList extends Many2ManyBinaryField {\n    static template = \"mail.MailComposerAttachmentList\";\n    /** @override */\n    setup() {\n        super.setup();\n        this.mailStore = useService(\"mail.store\");\n        this.attachmentUploadService = useService(\"mail.attachment_upload\");\n    }\n    /**\n     * @override\n     * @param {integer} fileId\n     */\n    async onFileRemove(fileId) {\n        super.onFileRemove(fileId);\n        const attachment = this.mailStore.Attachment.get(fileId);\n        if (attachment) {\n            await this.attachmentUploadService.unlink(attachment);\n        }\n    }\n}\n\nexport const mailComposerAttachmentList = {\n    ...many2ManyBinaryField,\n    component: MailComposerAttachmentList,\n};\n\nregistry.category(\"fields\").add(\"mail_composer_attachment_list\", mailComposerAttachmentList);\n", "import { dataUrlToBlob } from \"@mail/core/common/attachment_uploader_hook\";\nimport { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useX2ManyCrud } from \"@web/views/fields/relational_utils\";\n\nimport { Component } from \"@odoo/owl\";\nimport { FileUploader } from \"@web/views/fields/file_handler\";\n\n\nexport class MailComposerAttachmentSelector extends Component {\n    static template = \"mail.MailComposerAttachmentSelector\";\n    static components = { FileUploader };\n    static props = { ...standardFieldProps };\n\n    setup() {\n        this.mailStore = useService(\"mail.store\");\n        this.attachmentUploadService = useService(\"mail.attachment_upload\");\n        this.operations = useX2ManyCrud(() => {\n            return this.props.record.data[\"attachment_ids\"];\n        }, true);\n    }\n\n    /** @param {Object} data */\n    async onFileUploaded({ data, name, type }) {\n        const resIds = JSON.parse(this.props.record.data.res_ids);\n        const thread = await this.mailStore.Thread.insert({\n            model: this.props.record.data.model,\n            id: resIds[0],\n        });\n        const file = new File([dataUrlToBlob(data, type)], name, { type });\n        const attachment = await this.attachmentUploadService.upload(thread, thread.composer, file);\n        await this.operations.saveRecord([attachment.id]);\n    }\n}\n\nexport const mailComposerAttachmentSelector = {\n    component: MailComposerAttachmentSelector,\n};\n\nregistry.category(\"fields\").add(\"mail_composer_attachment_selector\", mailComposerAttachmentSelector);\n", "import { ChatGPTPromptDialog } from \"@html_editor/main/chatgpt/chatgpt_prompt_dialog\";\n\nimport { htmlJoin } from \"@mail/utils/common/html\";\n\nimport { Component, markup } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nexport class MailComposerChatGPT extends Component {\n    static template = \"mail.MailComposerChatGPT\";\n    static props = { ...standardFieldProps };\n\n    setup() {\n        this.btnLabel = _t(\"AI\"); // workaround to translate short string\n    }\n\n    async onOpenChatGPTPromptDialogBtnClick() {\n        this.env.services.dialog.add(ChatGPTPromptDialog, {\n            /** @param {DocumentFragment} content */\n            insert: (content) => {\n                const root = document.createElement(\"div\");\n                root.appendChild(content);\n                const { body } = this.props.record.data;\n                this.props.record.update({\n                    body: htmlJoin(body, markup(root.innerHTML)),\n                });\n            },\n            /**\n             * @param {HTMLElement} fragment\n             * @returns {string}\n             */\n            sanitize: (fragment) => {\n                return DOMPurify.sanitize(fragment, {\n                    IN_PLACE: true,\n                    ADD_TAGS: [\"#document-fragment\"],\n                    ADD_ATTR: [\"contenteditable\"],\n                });\n            },\n        });\n    }\n}\n\nexport const mailComposerChatGPT = {\n    component: MailComposerChatGPT,\n    fieldDependencies: [{ name: \"body\", type: \"text\" }],\n};\n\nregistry.category(\"fields\").add(\"mail_composer_chatgpt\", mailComposerChatGPT);\n", "import { registry } from \"@web/core/registry\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { Component, useState, onWillStart } from \"@odoo/owl\";\nimport { BaseRecipientsList } from \"@mail/core/web/base_recipients_list\";\n\nexport class MailComposerRecipientList extends Component {\n    static template = \"mail.MailComposerRecipientList\";\n    static components = { BaseRecipientsList };\n    static props = {\n        ...standardWidgetProps,\n        thread_model_field: { type: String },\n        thread_id_field: { type: String },\n    };\n\n    setup() {\n        const { Thread } = useService(\"mail.store\");\n        this.state = useState({});\n        onWillStart(async () => {\n            const threadIdFieldType = this.props.record.fields[this.props.thread_id_field].type;\n            let threadId;\n            if (threadIdFieldType === \"text\") {\n                // composer stores id in a string representing an array\n                threadId = JSON.parse(this.props.record.data[this.props.thread_id_field])[0];\n            } else if (threadIdFieldType === \"many2one_reference\") {\n                // scheduled message stores id as a many2one reference\n                threadId = this.props.record.data[this.props.thread_id_field].resId;\n            } else {\n                console.error(\"Thread id field type not supported\");\n                return;\n            }\n            try {\n                const thread = await Thread.getOrFetch({\n                    model: this.props.record.data[this.props.thread_model_field],\n                    id: threadId,\n                });\n                this.state.thread = thread;\n            } catch (e) {\n                console.error(e);\n            }\n        });\n    }\n}\n\nconst mailComposerRecipientList = {\n    component: MailComposerRecipientList,\n    extractProps: ({ attrs }) => ({\n        thread_model_field: attrs.thread_model_field,\n        thread_id_field: attrs.thread_id_field,\n    }),\n};\n\nregistry.category(\"view_widgets\").add(\"mail_composer_recipient_list\", mailComposerRecipientList);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { sprintf } from \"@web/core/utils/strings\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { Component, useState, onWillStart } from \"@odoo/owl\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { SelectCreateDialog } from \"@web/views/view_dialogs/select_create_dialog\";\n\n\nexport class MailComposerTemplateSelector extends Component {\n    static template = \"mail.MailComposerTemplateSelector\";\n    static components = { Dropdown, DropdownItem };\n    static props = { ...standardFieldProps };\n\n    setup() {\n        this.action = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.state = useState({});\n        this.limit = 7;\n\n        onWillStart(() => {\n            this.fetchTemplates();\n        });\n    }\n\n    async fetchTemplates() {\n        const fields = [\"display_name\"];\n        const templates = await this.orm.searchRead(\"mail.template\", [\n            [\"model\", \"=\", this.props.record.data.render_model],\n            [\"user_id\", \"=\", user.userId]\n        ], fields, { limit: this.limit });\n        if (templates.length < this.limit) {\n            templates.push(...await this.orm.searchRead(\"mail.template\", [\n                [\"model\", \"=\", this.props.record.data.render_model],\n                [\"user_id\", \"!=\", user.userId]\n            ], fields, { limit: this.limit - templates.length }));\n        }\n        this.state.templates = templates;\n    }\n\n    /**\n     * @param {Object} template\n     * @param {integer} template.id\n     * @param {string} template.display_name\n     */\n    async onLoadTemplate(template) {\n        await this.props.record.update({\n            template_id: [template.id]\n        });\n    }\n\n    /**\n     * @param {Object} template\n     * @param {integer} template.id\n     * @param {string} template.display_name\n     */\n    async onDeleteTemplate(template) {\n        this.env.services.dialog.add(ConfirmationDialog, {\n            body: sprintf(_t('Are you sure you want to delete \"%(template_name)s\"?'), {\n                template_name: template.display_name,\n            }),\n            confirmLabel: _t(\"Delete Template\"),\n            confirm: async () => {\n                await this.orm.unlink(\"mail.template\", [template.id]);\n                this.state.templates = this.state.templates.filter(current => {\n                    return current.id !== template.id;\n                });\n            },\n            cancel: () => {},\n        });\n    }\n\n    /**\n     * @param {Object} template\n     * @param {integer} template.id\n     * @param {string} template.display_name\n     */\n    async onOverwriteTemplate(template) {\n        this.env.services.dialog.add(ConfirmationDialog, {\n            body: sprintf(_t('Are your sure you want to update \"%(template_name)s\"?'), {\n                template_name: template.display_name,\n            }),\n            confirmLabel: _t(\"Update Template\"),\n            confirm: async () => {\n                await this.orm.write(\"mail.template\", [template.id], {\n                    subject: this.props.record.data.subject,\n                    body_html: this.props.record.data.body,\n                });\n            },\n            cancel: () => {},\n        });\n    }\n\n    async onSaveTemplate() {\n        if (!(await this.props.record.save())) {\n            return;\n        }\n        await this.action.doActionButton({\n            type: \"object\",\n            name: \"open_template_creation_wizard\",\n            resId: this.props.record.resId,\n            resModel: this.props.record.resModel\n        });\n    }\n\n    onSelectTemplateSearchMoreBtnClick() {\n        this.env.services.dialog.add(SelectCreateDialog, {\n            resModel: \"mail.template\",\n            title: _t(\"Insert Templates\"),\n            multiSelect: false,\n            noCreate: true,\n            domain: [[\"model\", \"=\", this.props.record.data.render_model]],\n            onSelected: async templateIds => {\n                await this.props.record.update({\n                    template_id: templateIds\n                });\n            },\n        });\n    }\n\n    onDeleteTemplateSearchMoreBtnClick() {\n        this.env.services.dialog.add(SelectCreateDialog, {\n            resModel: \"mail.template\",\n            title: _t(\"Delete Template\"),\n            multiSelect: true,\n            noCreate: true,\n            domain: [[\"model\", \"=\", this.props.record.data.render_model]],\n            onSelected: async templateIds => {\n                await this.orm.unlink(\"mail.template\", templateIds);\n                this.state.templates = this.state.templates.filter(current => {\n                    return !templateIds.includes(current.id);\n                });\n            },\n        });\n    }\n\n    onOverwriteTemplateSearchMoreBtnClick() {\n        this.env.services.dialog.add(SelectCreateDialog, {\n            resModel: \"mail.template\",\n            title: _t(\"Overwrite Template\"),\n            multiSelect: false,\n            noCreate: true,\n            domain: [[\"model\", \"=\", this.props.record.data.render_model]],\n            onSelected: async templateIds => {\n                await this.orm.write(\"mail.template\", templateIds, {\n                    subject: this.props.record.data.subject,\n                    body_html: this.props.record.data.body,\n                });\n            },\n        });\n    }\n}\n\nexport const mailComposerTemplateSelector = {\n    component: MailComposerTemplateSelector,\n    fieldDependencies: [\n        { name: \"can_edit_body\", type: \"boolean\" },\n        { name: \"render_model\", type: \"string\" },\n    ],\n};\n\nregistry.category(\"fields\").add(\"mail_composer_template_selector\", mailComposerTemplateSelector);\n", "import { patch } from \"@web/core/utils/patch\";\nimport { MailCoreCommon } from \"@mail/core/common/mail_core_common_service\";\n\npatch(MailCoreCommon.prototype, {\n    _handleNotificationToggleStar(payload, metadata) {\n        super._handleNotificationToggleStar(payload, metadata);\n        const { id: notifId } = metadata;\n        const { message_ids: messageIds, starred } = payload;\n        for (const id of messageIds) {\n            const message = this.store.Message.get({ id });\n            const starredBox = this.store.starred;\n            if (starred) {\n                if (notifId > starredBox.counter_bus_id) {\n                    starredBox.counter++;\n                }\n                starredBox.messages.add(message);\n            } else {\n                if (notifId > starredBox.counter_bus_id) {\n                    starredBox.counter--;\n                }\n                starredBox.messages.delete(message);\n            }\n        }\n    },\n});\n", "import { reactive } from \"@odoo/owl\";\n\nimport { registry } from \"@web/core/registry\";\n\nexport class MailCoreWeb {\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(\"mail.activity/updated\", (payload, { id: notifId }) => {\n            if (payload.activity_created && notifId > this.store.activity_counter_bus_id) {\n                this.store.activityCounter++;\n            }\n            if (payload.activity_deleted && notifId > this.store.activity_counter_bus_id) {\n                this.store.activityCounter--;\n            }\n        });\n        this.env.bus.addEventListener(\"mail.message/delete\", ({ detail: { message, notifId } }) => {\n            if (message.needaction && notifId > this.store.inbox.counter_bus_id) {\n                this.store.inbox.counter--;\n            }\n            if (message.starred && notifId > this.store.starred.counter_bus_id) {\n                this.store.starred.counter--;\n            }\n        });\n        this.busService.subscribe(\"mail.message/inbox\", (payload, { id: notifId }) => {\n            const { Message: messages = [] } = this.store.insert(payload, { html: true });\n            const [message] = messages;\n            const inbox = this.store.inbox;\n            if (notifId > inbox.counter_bus_id) {\n                inbox.counter++;\n            }\n            inbox.messages.add(message);\n            if (message.thread && notifId > message.thread.message_needaction_counter_bus_id) {\n                message.thread.message_needaction_counter++;\n            }\n            this.store.env.services[\"mail.out_of_focus\"].notify(message);\n        });\n        this.busService.subscribe(\"mail.message/mark_as_read\", (payload, { id: notifId }) => {\n            const { message_ids: messageIds, needaction_inbox_counter } = payload;\n            const inbox = this.store.inbox;\n            for (const messageId of messageIds) {\n                // We need to ignore all not yet known messages because we don't want them\n                // to be shown partially as they would be linked directly to cache.\n                // Furthermore, server should not send back all messageIds marked as read\n                // but something like last read messageId or something like that.\n                // (just imagine you mark 1000 messages as read ... )\n                const message = this.store.Message.get(messageId);\n                if (!message) {\n                    continue;\n                }\n                // update thread counter (before removing message from Inbox, to ensure isNeedaction check is correct)\n                const thread = message.thread;\n                if (\n                    thread &&\n                    message.needaction &&\n                    notifId > thread.message_needaction_counter_bus_id\n                ) {\n                    thread.message_needaction_counter--;\n                }\n                // move messages from Inbox to history\n                message.needaction = false;\n                inbox.messages.delete({ id: messageId });\n                const history = this.store.history;\n                history.messages.add(message);\n            }\n            if (notifId > inbox.counter_bus_id) {\n                inbox.counter = needaction_inbox_counter;\n                inbox.counter_bus_id = notifId;\n            }\n            if (inbox.counter > inbox.messages.length) {\n                inbox.fetchMoreMessages();\n            }\n        });\n        this.busService.start();\n    }\n}\n\nexport const mailCoreWeb = {\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 mailCoreWeb = reactive(new MailCoreWeb(env, services));\n        mailCoreWeb.setup();\n        return mailCoreWeb;\n    },\n};\n\nregistry.category(\"services\").add(\"mail.core.web\", mailCoreWeb);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Component, useEffect, useState } from \"@odoo/owl\";\nimport { useService, useAutofocus } from \"@web/core/utils/hooks\";\n\nimport { NavigableList } from \"@mail/core/common/navigable_list\";\nimport { useSequential } from \"@mail/utils/common/hooks\";\n\nexport class MentionList extends Component {\n    static template = \"mail.MentionList\";\n    static components = { NavigableList };\n    static props = {\n        onSelect: { type: Function },\n        close: { type: Function, optional: true },\n        type: { type: String },\n    };\n    static defaultProps = {\n        close: () => {},\n    };\n\n    setup() {\n        super.setup();\n        this.state = useState({\n            searchTerm: \"\",\n            options: [],\n            isFetching: false,\n        });\n        this.orm = useService(\"orm\");\n        this.store = useState(useService(\"mail.store\"));\n        this.suggestionService = useService(\"mail.suggestion\");\n        this.sequential = useSequential();\n        this.ref = useAutofocus({ mobile: true });\n\n        useEffect(\n            () => {\n                if (!this.state.searchTerm) {\n                    this.state.options = [];\n                    return;\n                }\n                this.sequential(async () => {\n                    this.state.isFetching = true;\n                    try {\n                        await this.suggestionService.fetchSuggestions({\n                            delimiter: this.props.type === \"partner\" ? \"@\" : \"#\",\n                            term: this.state.searchTerm,\n                        });\n                    } finally {\n                        this.state.isFetching = false;\n                    }\n                    const { suggestions } = this.suggestionService.searchSuggestions(\n                        {\n                            delimiter: this.props.type === \"partner\" ? \"@\" : \"#\",\n                            term: this.state.searchTerm,\n                        },\n                        { sort: true }\n                    );\n                    this.state.options = suggestions;\n                });\n            },\n            () => [this.state.searchTerm]\n        );\n    }\n\n    get placeholder() {\n        switch (this.props.type) {\n            case \"channel\":\n                return _t(\"Search for a channel...\");\n            case \"partner\":\n                return _t(\"Search for a user...\");\n            default:\n                return _t(\"Search...\");\n        }\n    }\n\n    get navigableListProps() {\n        const props = {\n            anchorRef: this.ref.el,\n            position: \"bottom-fit\",\n            isLoading: !!this.state.searchTerm && this.state.isFetching,\n            onSelect: (...args) => {\n                this.props.onSelect(...args);\n                this.props.close();\n            },\n            options: [],\n        };\n        switch (this.props.type) {\n            case \"partner\":\n                this.state.options.forEach((option) => {\n                    props.options.push({\n                        label: option.name,\n                        partner: option,\n                    });\n                });\n                break;\n            case \"channel\": {\n                this.state.options.forEach((option) => {\n                    props.options.push({\n                        label: option.name,\n                        channel: option,\n                    });\n                });\n                break;\n            }\n        }\n        return props;\n    }\n\n    onKeydown(ev) {\n        switch (ev.key) {\n            case \"Escape\": {\n                this.props.close();\n                break;\n            }\n        }\n    }\n}\n", "import { Message } from \"@mail/core/common/message\";\nimport { markEventHandled } from \"@web/core/utils/misc\";\n\nimport {\n    deserializeDate,\n    deserializeDateTime,\n    formatDate,\n    formatDateTime,\n} from \"@web/core/l10n/dates\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport {\n    formatChar,\n    formatFloat,\n    formatInteger,\n    formatMonetary,\n    formatText,\n} from \"@web/views/fields/formatters\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { AvatarCardPopover } from \"@mail/discuss/web/avatar_card/avatar_card_popover\";\n\npatch(Message.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.action = useService(\"action\");\n        this.avatarCard = usePopover(AvatarCardPopover);\n    },\n    get authorAvatarAttClass() {\n        return {\n            ...super.authorAvatarAttClass,\n            \"o_redirect cursor-pointer\": this.hasAuthorClickable(),\n        };\n    },\n    getAuthorAttClass() {\n        return {\n            ...super.getAuthorAttClass(),\n            \"cursor-pointer o-hover-text-underline\": this.hasAuthorClickable(),\n        };\n    },\n    getAuthorText() {\n        return this.hasAuthorClickable() ? _t(\"Open card\") : undefined;\n    },\n    getAvatarContainerAttClass() {\n        return {\n            ...super.getAvatarContainerAttClass(),\n            \"cursor-pointer\": this.hasAuthorClickable(),\n        };\n    },\n    hasAuthorClickable() {\n        return this.message.author?.userId;\n    },\n    onClickAuthor(ev) {\n        if (this.hasAuthorClickable()) {\n            markEventHandled(ev, \"Message.ClickAuthor\");\n            const target = ev.currentTarget;\n            if (!this.avatarCard.isOpen) {\n                this.avatarCard.open(target, {\n                    id: this.message.author.userId,\n                });\n            }\n        }\n    },\n    openRecord() {\n        this.message.thread.open();\n    },\n\n    /**\n     * @returns {string}\n     */\n    formatTracking(trackingType, trackingValue) {\n        switch (trackingType) {\n            case \"boolean\":\n                return trackingValue.value ? _t(\"Yes\") : _t(\"No\");\n            /**\n             * many2one formatter exists but is expecting id/display_name or data\n             * object but only the target record name is known in this context.\n             *\n             * Selection formatter exists but requires knowing all\n             * possibilities and they are not given in this context.\n             */\n            case \"char\":\n            case \"many2one\":\n            case \"selection\":\n                return formatChar(trackingValue.value);\n            case \"date\": {\n                const value = trackingValue.value\n                    ? deserializeDate(trackingValue.value)\n                    : trackingValue.value;\n                return formatDate(value);\n            }\n            case \"datetime\": {\n                const value = trackingValue.value\n                    ? deserializeDateTime(trackingValue.value)\n                    : trackingValue.value;\n                return formatDateTime(value);\n            }\n            case \"float\":\n                return formatFloat(trackingValue.value);\n            case \"integer\":\n                return formatInteger(trackingValue.value);\n            case \"text\":\n                return formatText(trackingValue.value);\n            case \"monetary\":\n                return formatMonetary(trackingValue.value, {\n                    currencyId: trackingValue.currencyId,\n                });\n            default:\n                return trackingValue.value;\n        }\n    },\n\n    /**\n     * @returns {string}\n     */\n    formatTrackingOrNone(trackingType, trackingValue) {\n        const formattedValue = this.formatTracking(trackingType, trackingValue);\n        return formattedValue || _t(\"None\");\n    },\n});\n", "import { MessagingMenu } from \"@mail/core/public_web/messaging_menu\";\nimport { onExternalClick } from \"@mail/utils/common/hooks\";\nimport { useEffect, useState } 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\";\nimport { MessagingMenuQuickSearch } from \"@mail/core/web/messaging_menu_quick_search\";\n\nObject.assign(MessagingMenu.components, { MessagingMenuQuickSearch });\n\npatch(MessagingMenu.prototype, {\n    setup() {\n        super.setup();\n        this.action = useService(\"action\");\n        this.pwa = useState(useService(\"pwa\"));\n        this.notification = useState(useService(\"mail.notification.permission\"));\n        Object.assign(this.state, {\n            searchOpen: false,\n        });\n\n        onExternalClick(\"selector\", () => Object.assign(this.state, { adding: false }));\n        useEffect(\n            () => {\n                if (\n                    this.store.discuss.searchTerm &&\n                    this.lastSearchTerm !== this.store.discuss.searchTerm &&\n                    this.state.activeIndex\n                ) {\n                    this.state.activeIndex = 0;\n                }\n                if (!this.store.discuss.searchTerm) {\n                    this.state.activeIndex = null;\n                }\n                this.lastSearchTerm = this.store.discuss.searchTerm;\n            },\n            () => [this.store.discuss.searchTerm]\n        );\n        useEffect(\n            () => {\n                if (!this.dropdown.isOpen) {\n                    this.state.activeIndex = null;\n                }\n            },\n            () => [this.dropdown.isOpen]\n        );\n    },\n    beforeOpen() {\n        this.state.searchOpen = false;\n        this.store.discuss.searchTerm = \"\";\n        this.store.isReady.then(() => {\n            if (\n                !this.store.inbox.isLoaded &&\n                this.store.inbox.status !== \"loading\" &&\n                this.store.inbox.counter !== this.store.inbox.messages.length\n            ) {\n                this.store.inbox.fetchNewMessages();\n            }\n        });\n    },\n    get canPromptToInstall() {\n        return this.pwa.canPromptToInstall;\n    },\n    get hasPreviews() {\n        return (\n            this.threads.length > 0 ||\n            (this.store.failures.length > 0 &&\n                this.store.discuss.activeTab === \"main\" &&\n                !this.env.inDiscussApp) ||\n            (this.shouldAskPushPermission &&\n                this.store.discuss.activeTab === \"main\" &&\n                !this.env.inDiscussApp) ||\n            (this.canPromptToInstall &&\n                this.store.discuss.activeTab === \"main\" &&\n                !this.env.inDiscussApp)\n        );\n    },\n    get installationRequest() {\n        return {\n            body: _t(\"Come here often? Install the app for quick and easy access!\"),\n            displayName: _t(\"Install Odoo\"),\n            onClick: () => {\n                this.pwa.show();\n            },\n            iconSrc: this.store.odoobot.avatarUrl,\n            partner: this.store.odoobot,\n            isShown: this.store.discuss.activeTab === \"main\" && this.canPromptToInstall,\n        };\n    },\n    get notificationRequest() {\n        return {\n            body: _t(\"Stay tuned! Enable push notifications to never miss a message.\"),\n            displayName: _t(\"Turn on notifications\"),\n            iconSrc: this.store.odoobot.avatarUrl,\n            partner: this.store.odoobot,\n            isShown: this.store.discuss.activeTab === \"main\" && this.shouldAskPushPermission,\n        };\n    },\n    get tabs() {\n        return [\n            {\n                icon: this.env.inDiscussApp ? \"fa fa-inbox\" : \"fa fa-envelope\",\n                id: \"main\",\n                label: this.env.inDiscussApp ? _t(\"Mailboxes\") : _t(\"All\"),\n            },\n            ...super.tabs,\n        ];\n    },\n    /** @param {import(\"models\").Failure} failure */\n    onClickFailure(failure) {\n        const threadIds = new Set(failure.notifications.map(({ message }) => message.thread.id));\n        if (threadIds.size === 1) {\n            const message = failure.notifications[0].message;\n            this.openThread(message.thread);\n        } else {\n            this.openFailureView(failure);\n            this.dropdown.close();\n        }\n    },\n    openThread(thread) {\n        if (this.store.discuss.isActive) {\n            this.action.doAction({\n                type: \"ir.actions.act_window\",\n                res_model: thread.model,\n                views: [[false, \"form\"]],\n                res_id: thread.id,\n            });\n            // Close the related chat window as having both the form view\n            // and the chat window does not look good.\n            this.store.ChatWindow.get({ thread })?.close();\n        } else {\n            thread.open({ fromMessagingMenu: true });\n        }\n        this.dropdown.close();\n    },\n    openFailureView(failure) {\n        if (failure.type !== \"email\") {\n            return;\n        }\n        this.action.doAction({\n            name: _t(\"Mail Failures\"),\n            type: \"ir.actions.act_window\",\n            view_mode: \"kanban,list,form\",\n            views: [\n                [false, \"kanban\"],\n                [false, \"list\"],\n                [false, \"form\"],\n            ],\n            target: \"current\",\n            res_model: failure.resModel,\n            domain: [[\"message_has_error\", \"=\", true]],\n            context: { create: false },\n        });\n    },\n    cancelNotifications(failure) {\n        return this.env.services.orm.call(failure.resModel, \"notify_cancel_by_type\", [], {\n            notification_type: failure.type,\n        });\n    },\n    toggleSearch() {\n        this.store.discuss.searchTerm = \"\";\n        this.state.searchOpen = !this.state.searchOpen;\n    },\n    get counter() {\n        let value =\n            this.store.inbox.counter +\n            this.store.failures.reduce((acc, f) => acc + parseInt(f.notifications.length), 0);\n        if (this.canPromptToInstall) {\n            value++;\n        }\n        if (this.shouldAskPushPermission) {\n            value++;\n        }\n        return value;\n    },\n    get displayStartConversation() {\n        return this.store.discuss.activeTab !== \"channel\" && !this.state.adding;\n    },\n    get shouldAskPushPermission() {\n        return (\n            this.notification.permission === \"prompt\" &&\n            !this.store.isNotificationPermissionDismissed\n        );\n    },\n    getFailureNotificationName(failure) {\n        if (failure.type === \"email\") {\n            return _t(\"Email Failure: %(modelName)s\", { modelName: failure.modelName });\n        }\n        return _t(\"Failure: %(modelName)s\", { modelName: failure.modelName });\n    },\n});\n", "import { onExternalClick } from \"@mail/utils/common/hooks\";\nimport { Component, useState } from \"@odoo/owl\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\n\nimport { useAutofocus, useService } from \"@web/core/utils/hooks\";\n\nexport class MessagingMenuQuickSearch extends Component {\n    static components = {};\n    static props = [\"onClose\"];\n    static template = \"mail.MessagingMenuQuickSearch\";\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n        useAutofocus();\n        onExternalClick(\"search\", () => this.props.onClose());\n    }\n\n    onKeydownInput(ev) {\n        const hotkey = getActiveHotkey(ev);\n        if (hotkey === \"escape\") {\n            ev.stopPropagation();\n            ev.preventDefault();\n            this.props.onClose();\n        }\n    }\n}\n", "import { useService } from \"@web/core/utils/hooks\";\n\nexport const helpers = {\n    SUPPORTED_M2X_AVATAR_MODELS: [\"res.users\"],\n    buildOpenChatParams: (resModel, id) => {\n        if (resModel === \"res.users\") {\n            return { userId: id };\n        }\n    },\n};\n\nexport function useOpenChat(resModel) {\n    const store = useService(\"mail.store\");\n    if (!helpers.SUPPORTED_M2X_AVATAR_MODELS.includes(resModel)) {\n        throw new Error(\n            `This widget is only supported on many2one and many2many fields pointing to ${JSON.stringify(\n                helpers.SUPPORTED_M2X_AVATAR_MODELS\n            )}`\n        );\n    }\n    return async (id) => {\n        store.openChat(helpers.buildOpenChatParams(resModel, id));\n    };\n}\n", "import { useVisible } from \"@mail/utils/common/hooks\";\nimport { Component, useState } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { sprintf } from \"@web/core/utils/strings\";\n\n/**\n * @typedef {Object} Props\n * @property {import('@mail/core/common/thread_model').Thread} thread\n * @property {function} [close]\n * @extends {Component<Props, Env>}\n */\nexport class RecipientList extends Component {\n    static template = \"mail.RecipientList\";\n    static props = [\"thread\", \"close?\"];\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n        this.loadMoreState = useVisible(\"load-more\", () => {\n            if (this.loadMoreState.isVisible) {\n                this.props.thread.loadMoreRecipients();\n            }\n        });\n    }\n\n    getRecipientText(recipient) {\n        return (\n            recipient.partner.email ||\n            sprintf(_t(\"[%(name)s] (no email address)\"), { name: recipient.partner.name })\n        );\n    }\n}\n", "import { Record } from \"@mail/core/common/record\";\nimport { Store } from \"@mail/core/common/store_service\";\nimport { compareDatetime } from \"@mail/utils/common/misc\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {import(\"models\").Store} */\nconst StorePatch = {\n    setup() {\n        super.setup(...arguments);\n        this.activityCounter = 0;\n        this.activity_counter_bus_id = 0;\n        this.activityGroups = Record.attr([], {\n            onUpdate() {\n                this.onUpdateActivityGroups();\n            },\n            sort(g1, g2) {\n                /**\n                 * Sort by model ID ASC but always place the activity group for \"mail.activity\" model at\n                 * the end (other activities).\n                 */\n                const getSortId = (activityGroup) =>\n                    activityGroup.model === \"mail.activity\" ? Number.MAX_VALUE : activityGroup.id;\n                return getSortId(g1) - getSortId(g2);\n            },\n        });\n        this.inbox = Record.one(\"Thread\");\n        this.starred = Record.one(\"Thread\");\n        this.history = Record.one(\"Thread\");\n    },\n    onStarted() {\n        super.onStarted(...arguments);\n        this.inbox = {\n            id: \"inbox\",\n            model: \"mail.box\",\n            name: _t(\"Inbox\"),\n        };\n        this.starred = {\n            id: \"starred\",\n            model: \"mail.box\",\n            name: _t(\"Starred\"),\n        };\n        this.history = {\n            id: \"history\",\n            model: \"mail.box\",\n            name: _t(\"History\"),\n        };\n        try {\n            // useful for synchronizing activity data between multiple tabs\n            this.activityBroadcastChannel = new browser.BroadcastChannel(\"mail.activity.channel\");\n            this.activityBroadcastChannel.onmessage =\n                this._onActivityBroadcastChannelMessage.bind(this);\n        } catch {\n            // BroadcastChannel API is not supported (e.g. Safari < 15.4), so disabling it.\n            this.activityBroadcastChannel = null;\n        }\n    },\n    get initMessagingParams() {\n        return {\n            ...super.initMessagingParams,\n            failures: true,\n            systray_get_activities: true,\n        };\n    },\n    getNeedactionChannels() {\n        return this.getRecentChannels().filter((channel) => channel.importantCounter > 0);\n    },\n    getRecentChannels() {\n        return Object.values(this.Thread.records)\n            .filter((thread) => thread.model === \"discuss.channel\")\n            .sort((a, b) => compareDatetime(b.lastInterestDt, a.lastInterestDt) || b.id - a.id);\n    },\n    onUpdateActivityGroups() {},\n    async scheduleActivity(resModel, resIds, defaultActivityTypeId = undefined) {\n        const context = {\n            active_model: resModel,\n            active_ids: resIds,\n            active_id: resIds[0],\n            ...(defaultActivityTypeId !== undefined\n                ? { default_activity_type_id: defaultActivityTypeId }\n                : {}),\n        };\n        return new Promise((resolve) =>\n            this.env.services.action.doAction(\n                {\n                    type: \"ir.actions.act_window\",\n                    name:\n                        resIds && resIds.length > 1\n                            ? _t(\"Schedule Activity On Selected Records\")\n                            : _t(\"Schedule Activity\"),\n                    res_model: \"mail.activity.schedule\",\n                    view_mode: \"form\",\n                    views: [[false, \"form\"]],\n                    target: \"new\",\n                    context,\n                },\n                { onClose: resolve }\n            )\n        );\n    },\n    _onActivityBroadcastChannelMessage({ data }) {\n        switch (data.type) {\n            case \"INSERT\":\n                this.Activity.insert(data.payload, { broadcast: false, html: true });\n                break;\n            case \"DELETE\": {\n                const activity = this.Activity.insert(data.payload, { broadcast: false });\n                activity.remove({ broadcast: false });\n                break;\n            }\n            case \"RELOAD_CHATTER\": {\n                const thread = this.Thread.insert({\n                    model: data.payload.model,\n                    id: data.payload.id,\n                });\n                thread.fetchNewMessages();\n                break;\n            }\n        }\n    },\n    async unstarAll() {\n        // apply the change immediately for faster feedback\n        this.store.starred.counter = 0;\n        this.store.starred.messages = [];\n        await this.env.services.orm.call(\"mail.message\", \"unstar_all\");\n    },\n    handleClickOnLink(ev, thread) {\n        const model = ev.target.dataset.oeModel;\n        const id = Number(ev.target.dataset.oeId);\n        const isLinkHandledBySuper = super.handleClickOnLink(...arguments);\n        if (!isLinkHandledBySuper && ev.target.tagName === \"A\" && id && model) {\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    onLinkFollowed(fromThread) {\n        if (!this.env.isSmall && fromThread?.model === \"discuss.channel\") {\n            fromThread.open(true, { autofocus: false });\n        }\n    },\n};\npatch(Store.prototype, StorePatch);\n", "import { Component } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { FormViewDialog } from \"@web/views/view_dialogs/form_view_dialog\";\n\n/**\n * @typedef {Object} Props\n * @property {function} onSuggestedRecipientAdded\n * @property {import(\"models\").Thread} thread\n * @property {import(\"@mail/core/web/suggested_recipient\").SuggestedRecipient} recipient\n * @extends {Component<Props, Env>}\n */\nexport class SuggestedRecipient extends Component {\n    static template = \"mail.SuggestedRecipients\";\n    static props = [\"thread\", \"recipient\", \"onSuggestedRecipientAdded\"];\n\n    setup() {\n        super.setup();\n        this.dialogService = useService(\"dialog\");\n    }\n\n    get titleText() {\n        return _t(\"Add as recipient and follower (reason: %s)\", this.props.recipient.reason);\n    }\n\n    onChangeCheckbox(ev) {\n        this.props.recipient.checked = !this.props.recipient.checked;\n        if (this.props.recipient.checked && !this.props.recipient.persona) {\n            this.props.recipient.checked = false;\n            // Recipients must always be partners. On selecting a suggested\n            // recipient that does not have a partner, the partner creation form\n            // should be opened.\n            const thread = this.props.thread;\n            this.dialogService.add(FormViewDialog, {\n                context: {\n                    active_id: thread.id,\n                    active_model: \"mail.compose.message\",\n                    default_email: this.props.recipient.email,\n                    default_name: this.props.recipient.name,\n                    default_lang: this.props.recipient.lang,\n                    ...Object.fromEntries(\n                        Object.entries(this.props.recipient.create_values).map(([k, v]) => [\n                            \"default_\" + k,\n                            v,\n                        ])\n                    ),\n                    force_email: true,\n                    ref: \"compound_context\",\n                },\n                onRecordSaved: () => this.props.onSuggestedRecipientAdded(thread),\n                resModel: \"res.partner\",\n                title: _t(\"Please complete customer's information\"),\n            });\n        }\n    }\n}\n", "import { SuggestedRecipient } from \"@mail/core/web/suggested_recipient\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\n/**\n * @typedef {Object} Props\n * @property {function} onSuggestedRecipientAdded\n * @property {import(\"models\").Thread} thread\n * @property {string} className\n * @property {string} styleString\n * @extends {Component<Props, Env>}\n */\nexport class SuggestedRecipientsList extends Component {\n    static template = \"mail.SuggestedRecipientsList\";\n    static components = { SuggestedRecipient };\n    static props = [\"thread\", \"className?\", \"styleString?\", \"onSuggestedRecipientAdded\"];\n\n    setup() {\n        super.setup();\n        this.state = useState({ showMore: false });\n    }\n\n    get suggestedRecipients() {\n        if (!this.state.showMore) {\n            return this.props.thread.suggestedRecipients.slice(0, 3);\n        }\n        return this.props.thread.suggestedRecipients;\n    }\n}\n", "import { threadActionsRegistry } from \"@mail/core/common/thread_actions\";\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(\"mark-all-read\", {\n        condition(component) {\n            return component.thread?.id === \"inbox\";\n        },\n        disabledCondition(component) {\n            return component.thread.isEmpty && !component.store.inbox.counter;\n        },\n        open(component) {\n            component.orm.silent.call(\"mail.message\", \"mark_all_as_read\");\n        },\n        sequence: 1,\n        text: _t(\"Mark all read\"),\n    })\n    .add(\"unstar-all\", {\n        condition(component) {\n            return component.thread?.id === \"starred\";\n        },\n        disabledCondition(component) {\n            return component.thread.isEmpty;\n        },\n        open(component) {\n            component.store.unstarAll();\n        },\n        sequence: 2,\n        setup() {\n            const component = useComponent();\n            component.store = useState(useService(\"mail.store\"));\n        },\n        text: _t(\"Unstar all\"),\n    })\n    .add(\"expand-form\", {\n        condition(component) {\n            return (\n                component.thread &&\n                ![\"mail.box\", \"discuss.channel\"].includes(component.thread.model) &&\n                component.props.chatWindow?.isOpen\n            );\n        },\n        setup() {\n            const component = useComponent();\n            component.actionService = useService(\"action\");\n        },\n        icon: \"fa fa-fw fa-expand\",\n        name: _t(\"Open Form View\"),\n        open(component) {\n            component.actionService.doAction({\n                type: \"ir.actions.act_window\",\n                res_id: component.thread.id,\n                res_model: component.thread.model,\n                views: [[false, \"form\"]],\n            });\n            component.props.chatWindow.close();\n        },\n        sequence: 40,\n        sequenceGroup: 20,\n    });\n", "import { Thread } from \"@mail/core/common/thread_model\";\n\nimport { patch } from \"@web/core/utils/patch\";\nimport { Record } from \"../common/record\";\nimport { compareDatetime } from \"@mail/utils/common/misc\";\n\npatch(Thread.prototype, {\n    /** @type {integer|undefined} */\n    recipientsCount: undefined,\n    setup() {\n        super.setup();\n        this.recipients = Record.many(\"Follower\");\n        this.activities = Record.many(\"Activity\", {\n            sort: (a, b) => compareDatetime(a.date_deadline, b.date_deadline) || a.id - b.id,\n            onDelete(r) {\n                r.remove();\n            },\n        });\n    },\n    get recipientsFullyLoaded() {\n        return this.recipientsCount === this.recipients.length;\n    },\n    closeChatWindow() {\n        const chatWindow = this.store.ChatWindow.get({ thread: this });\n        chatWindow?.close({ notifyState: false });\n    },\n    computeIsDisplayed() {\n        if (this.store.discuss.isActive && !this.store.env.services.ui.isSmall) {\n            return this.eq(this.store.discuss.thread);\n        }\n        return super.computeIsDisplayed();\n    },\n    async leave() {\n        this.closeChatWindow();\n        super.leave(...arguments);\n    },\n    async loadMoreFollowers() {\n        const data = await this.store.env.services.orm.call(this.model, \"message_get_followers\", [\n            [this.id],\n            this.followers.at(-1).id,\n        ]);\n        this.store.insert(data);\n    },\n    async loadMoreRecipients() {\n        const data = await this.store.env.services.orm.call(\n            this.model,\n            \"message_get_followers\",\n            [[this.id], this.recipients.at(-1).id],\n            { filter_recipients: true }\n        );\n        this.store.insert(data);\n    },\n    open(options) {\n        if (this.model === \"discuss.channel\" && !this.selfMember) {\n            this.store.env.services[\"bus_service\"].addChannel(this.busChannel);\n        }\n        if (!this.store.discuss.isActive && !this.store.env.services.ui.isSmall) {\n            this.openChatWindow(options);\n            return;\n        }\n        if (this.store.env.services.ui.isSmall && this.model === \"discuss.channel\") {\n            this.openChatWindow(options);\n            return;\n        }\n        if (this.model !== \"discuss.channel\") {\n            this.store.env.services.action.doAction({\n                type: \"ir.actions.act_window\",\n                res_id: this.id,\n                res_model: this.model,\n                views: [[false, \"form\"]],\n            });\n            return;\n        }\n        super.open();\n    },\n    async unpin() {\n        const chatWindow = this.store.ChatWindow.get({ thread: this });\n        await chatWindow?.close();\n        super.unpin(...arguments);\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 {\n    createDocumentFragmentFromContent,\n    htmlJoin,\n    htmlReplace,\n    htmlTrim,\n} from \"@mail/utils/common/html\";\n\nimport { markup } from \"@odoo/owl\";\n\nimport { stateToUrl } from \"@web/core/browser/router\";\nimport { loadEmoji } from \"@web/core/emoji_picker/emoji_picker\";\nimport { htmlEscape, setElementContent } from \"@web/core/utils/html\";\nimport { escapeRegExp, unaccent } from \"@web/core/utils/strings\";\nimport { setAttributes } from \"@web/core/utils/xml\";\n\nconst urlRegexp =\n    /\\b(?:https?:\\/\\/\\d{1,3}(?:\\.\\d{1,3}){3}|(?:https?:\\/\\/|(?:www\\.))[-a-z0-9@:%._+~#=\\u00C0-\\u024F\\u1E00-\\u1EFF]{1,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|ReturnType<markup>}\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 = htmlReplace(escapedAndCompactContent, /&nbsp;/g, \" \");\n    body = htmlTrim(body);\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 = await _generateEmojisOnHtml(body);\n    body = parseAndTransform(body, addLink);\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|ReturnType<markup>} htmlString\n * @param {function} transformFunction\n * @returns {ReturnType<markup>}\n */\nexport function parseAndTransform(htmlString, transformFunction) {\n    let children;\n    try {\n        const div = document.createElement(\"div\");\n        setElementContent(div, htmlString);\n        children = Array.from(div.childNodes);\n    } catch {\n        const div = document.createElement(\"div\");\n        const pre = document.createElement(\"pre\");\n        setElementContent(pre, htmlString);\n        div.appendChild(pre);\n        children = Array.from(div.childNodes);\n    }\n    return _parseAndTransform(children, transformFunction);\n}\n\n/**\n * @param {Node[]} nodes\n * @param {function} transformFunction with:\n *   param node\n *   param function\n *   return string\n * @return {ReturnType<markup>}\n */\nfunction _parseAndTransform(nodes, transformFunction) {\n    if (!nodes) {\n        return;\n    }\n    return htmlJoin(\n        ...Object.values(nodes).map((node) =>\n            transformFunction(node, function () {\n                return _parseAndTransform(node.childNodes, transformFunction);\n            })\n        )\n    );\n}\n\n/**\n * @param {string} text\n * @return {ReturnType<markup>} 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 = htmlJoin(result, 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 = htmlJoin(\n            result,\n            markup(\n                `<a target=\"_blank\" rel=\"noreferrer noopener\" href=\"${href}\">${_escapeEntities(\n                    url\n                )}</a>`\n            )\n        );\n        curIndex = match.index + match[0].length;\n    }\n    return htmlJoin(result, text.slice(curIndex));\n}\n\n/**\n * @param {Node} node\n * @param {function} transformFunction\n * @return {ReturnType<markup>}\n */\nexport function addLink(node, transformChildren) {\n    if (node.nodeType === 3) {\n        // text node\n        const linkified = linkify(node.textContent);\n        if (linkified.toString() !== node.textContent) {\n            const div = document.createElement(\"div\");\n            setElementContent(div, 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 markup(node.outerHTML);\n    }\n    transformChildren();\n    return markup(node.outerHTML);\n}\n\n/**\n * Returns an escaped conversion of a content.\n *\n * @param {string|ReturnType<markup>} content\n * @returns {ReturnType<markup>}\n */\nexport function escapeAndCompactTextContent(content) {\n    //Removing unwanted extra spaces from message\n    let value = htmlTrim(content);\n    value = htmlReplace(value, /(\\r|\\n){2,}/g, markup(\"<br/><br/>\"));\n    value = htmlReplace(value, /(\\r|\\n)/g, markup(\"<br/>\"));\n\n    // prevent html space collapsing\n    value = htmlReplace(value, / /g, markup(\"&nbsp;\"));\n    value = htmlReplace(value, /([^>])&nbsp;([^<])/g, markup(\"$1 $2\"));\n    return value;\n}\n\n/**\n * @param body {string|ReturnType<markup>}\n * @param validRecords {Object}\n * @param validRecords.partners {Array}\n * @return {ReturnType<markup>}\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 = `@${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 = htmlReplace(body, 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 = `#${thread.parent_channel_id.displayName} > ${thread.displayName}`;\n        } else {\n            className = \"o_channel_redirect\";\n            text = `#${thread.displayName}`;\n        }\n        mentions.push({\n            class: className,\n            id: thread.id,\n            model: \"discuss.channel\",\n            placeholder,\n            text,\n        });\n        body = htmlReplace(body, text, placeholder);\n    }\n    for (const special of specialMentions) {\n        body = htmlReplace(\n            body,\n            `@${special}`,\n            markup(`<a href=\"#\" class=\"o-discuss-mention\">@${htmlEscape(special)}</a>`)\n        );\n    }\n    for (const mention of mentions) {\n        const link = document.createElement(\"a\");\n        setAttributes(link, {\n            href: stateToUrl({ model: mention.model, resId: mention.id }),\n            class: mention.class,\n            \"data-oe-id\": mention.id,\n            \"data-oe-model\": mention.model,\n            target: \"_blank\",\n            contenteditable: \"false\",\n        });\n        link.textContent = mention.text;\n        body = htmlReplace(body, mention.placeholder, markup(link.outerHTML));\n    }\n    return htmlEscape(body);\n}\n\n/**\n * @private\n * @param {string|ReturnType<markup>} htmlString\n * @returns {ReturnType<markup>}\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 = htmlJoin(String(source));\n            const regexp = new RegExp(\"(\\\\s|^)(\" + escapeRegExp(escapedSource) + \")(?=\\\\s|$)\", \"g\");\n            htmlString = htmlReplace(htmlString, regexp, \"$1\" + emoji.codepoints);\n        }\n    }\n    return htmlEscape(htmlString);\n}\n\n/**\n * @param {string|ReturnType<markup>} htmlString\n * @returns {string}\n */\nexport function htmlToTextContentInline(htmlString) {\n    htmlString = htmlReplace(htmlString, /<br\\s*\\/?>/gi, \" \");\n    const div = document.createElement(\"div\");\n    try {\n        setElementContent(div, htmlString);\n    } catch {\n        const pre = document.createElement(\"pre\");\n        setElementContent(pre, htmlString);\n        div.appendChild(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    str = htmlReplace(str, /<br\\s*\\/?>/gi, \"\\n\");\n    return createDocumentFragmentFromContent(str).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 * Hook that allows to determine precisely when refs are (mouse-)hovered.\n * Should provide a list of ref names, and can add callbacks when elements are\n * hovered-in (onHover), hovered-out (onAway), hovering for some time (onHovering).\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 * @param {() => Array} [param1.stateObserver] when provided, function that, when called, returns list of\n *   reactive state related to presence of targets' el. This is used to help the hook detect when the targets\n *   are removed from DOM, to properly mark the hovered target as non-hovered.\n * @returns {({ isHover: boolean })}\n */\nexport function useHover(refNames, { onHover, onAway, stateObserver, onHovering } = {}) {\n    refNames = Array.isArray(refNames) ? refNames : [refNames];\n    const targets = [];\n    let wasHovering = false;\n    let hoveringTimeout;\n    let awayTimeout;\n    let lastHoveredTarget;\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                lastHoveredTarget = target;\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        lastHoveredTarget = null;\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\n    if (stateObserver) {\n        useEffect(() => {\n            if (lastHoveredTarget && !lastHoveredTarget.ref.el) {\n                setHover(false);\n                lastHoveredTarget = null;\n            }\n        }, stateObserver);\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\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 { markup } from \"@odoo/owl\";\n\nimport { htmlEscape, setElementContent } from \"@web/core/utils/html\";\n\n/**\n * Safely creates a Document fragment from content. If content was flagged as safe HTML using\n * `markup()` it is parsed as HTML. Otherwise it is escaped and parsed as text.\n *\n * @param {string|ReturnType<markup>} content\n */\nexport function createDocumentFragmentFromContent(content) {\n    const div = document.createElement(\"div\");\n    setElementContent(div, content);\n    return new DOMParser().parseFromString(div.innerHTML, \"text/html\");\n}\n\n/**\n * Applies list join on content and returns a markup result built for HTML.\n *\n * @param {Array<string|ReturnType<markup>>} args\n * @returns {ReturnType<markup>}\n */\nexport function htmlJoin(...args) {\n    return markup(args.map((arg) => htmlEscape(arg)).join(\"\"));\n}\n\n/**\n * Applies string replace on content and returns a markup result built for HTML.\n *\n * @param {string|ReturnType<markup>} content\n * @param {string | RegExp} search\n * @param {string} replacement\n * @returns {ReturnType<markup>}\n */\nexport function htmlReplace(content, search, replacement) {\n    content = htmlEscape(content);\n    if (typeof search === \"string\" || search instanceof String) {\n        search = htmlEscape(search);\n    }\n    replacement = htmlEscape(replacement);\n    return markup(content.replace(search, replacement));\n}\n\n/**\n * Applies string trim on content and returns a markup result built for HTML.\n *\n * @param {string|ReturnType<markup>} content\n * @returns {string|ReturnType<markup>}\n */\nexport function htmlTrim(content) {\n    content = htmlEscape(content);\n    return markup(content.trim());\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 { Composer } from \"@mail/core/common/composer\";\nimport { Thread } from \"@mail/core/common/thread\";\n\nimport {\n    Component,\n    onMounted,\n    onWillUpdateProps,\n    useChildSubEnv,\n    useRef,\n    useState,\n} from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useThrottleForAnimation } from \"@web/core/utils/timing\";\n\n/**\n * @typedef {Object} Props\n * @extends {Component<Props, Env>}\n */\nexport class Chatter extends Component {\n    static template = \"mail.Chatter\";\n    static components = { Thread, Composer };\n    static props = [\"composer?\", \"threadId?\", \"threadModel\", \"twoColumns?\"];\n    static defaultProps = { composer: true, threadId: false, twoColumns: false };\n\n    setup() {\n        this.store = useState(useService(\"mail.store\"));\n        this.state = useState({\n            jumpThreadPresent: 0,\n            /** @type {import(\"models\").Thread} */\n            thread: undefined,\n            aside: false,\n        });\n        this.rootRef = useRef(\"root\");\n        this.onScrollDebounced = useThrottleForAnimation(this.onScroll);\n        useChildSubEnv(this.childSubEnv);\n\n        onMounted(this._onMounted);\n        onWillUpdateProps((nextProps) => {\n            if (\n                this.props.threadId !== nextProps.threadId ||\n                this.props.threadModel !== nextProps.threadModel\n            ) {\n                this.changeThread(nextProps.threadModel, nextProps.threadId);\n            }\n            if (!this.env.chatter || this.env.chatter?.fetchData) {\n                if (this.env.chatter) {\n                    this.env.chatter.fetchData = false;\n                }\n                this.load(this.state.thread, this.requestList);\n            }\n        });\n    }\n\n    get afterPostRequestList() {\n        return [\"messages\"];\n    }\n\n    get childSubEnv() {\n        return { inChatter: this.state };\n    }\n\n    get onCloseFullComposerRequestList() {\n        return [\"messages\"];\n    }\n\n    get requestList() {\n        return [];\n    }\n\n    changeThread(threadModel, threadId) {\n        this.state.thread = this.store.Thread.insert({ model: threadModel, id: threadId });\n        if (threadId === false) {\n            if (this.state.thread.messages.length === 0) {\n                this.state.thread.messages.push({\n                    id: this.store.getNextTemporaryId(),\n                    author: this.store.self,\n                    body: _t(\"Creating a new record...\"),\n                    message_type: \"notification\",\n                    thread: this.state.thread,\n                    trackingValues: [],\n                    res_id: threadId,\n                    model: threadModel,\n                });\n            }\n        }\n    }\n\n    /**\n     * Fetch data for the thread according to the request list.\n     * @param {import(\"models\").Thread} thread\n     * @param {string[]} requestList\n     */\n    load(thread, requestList) {\n        if (!thread.id || !this.state.thread?.eq(thread)) {\n            return;\n        }\n        thread.fetchData(requestList);\n    }\n\n    onCloseFullComposerCallback() {\n        this.load(this.state.thread, this.onCloseFullComposerRequestList);\n    }\n\n    _onMounted() {\n        this.changeThread(this.props.threadModel, this.props.threadId);\n        if (!this.env.chatter || this.env.chatter?.fetchData) {\n            if (this.env.chatter) {\n                this.env.chatter.fetchData = false;\n            }\n            this.load(this.state.thread, this.requestList);\n        }\n    }\n\n    onPostCallback() {\n        this.state.jumpThreadPresent++;\n        // Load new messages to fetch potential new messages from other users (useful due to lack of auto-sync in chatter).\n        this.load(this.state.thread, this.afterPostRequestList);\n    }\n\n    onScroll() {\n        this.state.isTopStickyPinned = this.rootRef.el.scrollTop !== 0;\n    }\n}\n", "import { patch } from \"@web/core/utils/patch\";\nimport { browser } from \"@web/core/browser/browser\";\n\nimport { Chatter } from \"@mail/chatter/web_portal/chatter\";\n\npatch(Chatter.prototype, {\n    setup() {\n        super.setup();\n        const showNotificationMessages = browser.localStorage.getItem(\n            'muk_web_chatter.notifications'\n        );\n        this.state.showNotificationMessages = (\n            showNotificationMessages != null ? \n            JSON.parse(showNotificationMessages) : true\n        );\n    },\n    onClickNotificationsToggle() {\n        const showNotificationMessages = !this.state.showNotificationMessages;\n        browser.localStorage.setItem(\n            'muk_web_chatter.notifications', showNotificationMessages\n        );\n        this.state.showNotificationMessages = showNotificationMessages;\n    },\n});\n\n\n", "import { Composer } from \"@mail/core/common/composer\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Composer.prototype, {\n    get placeholder() {\n        if (this.thread && this.thread.model !== \"discuss.channel\" && !this.props.placeholder) {\n            if (this.props.type === \"message\") {\n                return _t(\"Send a message to followers\u2026\");\n            } else {\n                return _t(\"Log an internal note\u2026\");\n            }\n        }\n        return super.placeholder;\n    },\n});\n", "import { Thread } from \"@mail/core/common/thread_model\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Thread.prototype, {\n    /** @param {string[]} requestList */\n    async fetchData(requestList) {\n        if (requestList.includes(\"messages\")) {\n            this.fetchNewMessages();\n        }\n        const result = await rpc(\"/mail/thread/data\", {\n            request_list: requestList,\n            thread_id: this.id,\n            thread_model: this.model,\n            ...this.rpcParams,\n        });\n        this.store.insert(result, { html: true });\n    },\n});\n", "import { ScheduledMessage } from \"@mail/chatter/web/scheduled_message\";\nimport { Activity } from \"@mail/core/web/activity\";\nimport { AttachmentList } from \"@mail/core/common/attachment_list\";\nimport { BaseRecipientsList } from \"@mail/core/web/base_recipients_list\";\nimport { Chatter } from \"@mail/chatter/web_portal/chatter\";\nimport { SuggestedRecipientsList } from \"@mail/core/web/suggested_recipient_list\";\nimport { FollowerList } from \"@mail/core/web/follower_list\";\nimport { isDragSourceExternalFile } from \"@mail/utils/common/misc\";\nimport { useAttachmentUploader } from \"@mail/core/common/attachment_uploader_hook\";\nimport { useCustomDropzone } from \"@web/core/dropzone/dropzone_hook\";\nimport { useHover, useMessageHighlight } from \"@mail/utils/common/hooks\";\nimport { MailAttachmentDropzone } from \"@mail/core/common/mail_attachment_dropzone\";\nimport { SearchMessageInput } from \"@mail/core/common/search_message_input\";\nimport { SearchMessageResult } from \"@mail/core/common/search_message_result\";\n\nimport { useEffect } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { FileUploader } from \"@web/views/fields/file_handler\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useMessageSearch } from \"@mail/core/common/message_search_hook\";\nimport { usePopoutAttachment } from \"@mail/core/common/attachment_view\";\n\nexport const DELAY_FOR_SPINNER = 1000;\n\nObject.assign(Chatter.components, {\n    Activity,\n    AttachmentList,\n    BaseRecipientsList,\n    Dropdown,\n    FileUploader,\n    FollowerList,\n    ScheduledMessage,\n    SearchMessageInput,\n    SearchMessageResult,\n    SuggestedRecipientsList,\n});\n\nChatter.props.push(\n    \"close?\",\n    \"compactHeight?\",\n    \"has_activities?\",\n    \"hasAttachmentPreview?\",\n    \"hasParentReloadOnAttachmentsChanged?\",\n    \"hasParentReloadOnFollowersUpdate?\",\n    \"hasParentReloadOnMessagePosted?\",\n    \"highlightMessageId?\",\n    \"isAttachmentBoxVisibleInitially?\",\n    \"isChatterAside?\",\n    \"isInFormSheetBg?\",\n    \"saveRecord?\",\n    \"webRecord?\"\n);\n\nObject.assign(Chatter.defaultProps, {\n    compactHeight: false,\n    has_activities: true,\n    hasAttachmentPreview: false,\n    hasParentReloadOnAttachmentsChanged: false,\n    hasParentReloadOnFollowersUpdate: false,\n    hasParentReloadOnMessagePosted: false,\n    isAttachmentBoxVisibleInitially: false,\n    isChatterAside: false,\n    isInFormSheetBg: true,\n});\n\n/**\n * @type {import(\"@mail/chatter/web_portal/chatter\").Chatter }\n * @typedef {Object} Props\n * @property {function} [close]\n */\npatch(Chatter.prototype, {\n    setup() {\n        this.messageHighlight = useMessageHighlight();\n        super.setup(...arguments);\n        this.orm = useService(\"orm\");\n        this.attachmentPopout = usePopoutAttachment();\n        Object.assign(this.state, {\n            composerType: false,\n            isAttachmentBoxOpened: this.props.isAttachmentBoxVisibleInitially,\n            isSearchOpen: false,\n            showActivities: true,\n            showAttachmentLoading: false,\n            showScheduledMessages: true,\n        });\n        this.messageSearch = useMessageSearch();\n        this.attachmentUploader = useAttachmentUploader(\n            this.store.Thread.insert({ model: this.props.threadModel, id: this.props.threadId })\n        );\n        this.unfollowHover = useHover(\"unfollow\");\n        this.followerListDropdown = useDropdownState();\n        /** @type {number|null} */\n        this.loadingAttachmentTimeout = null;\n        useCustomDropzone(this.rootRef, MailAttachmentDropzone, {\n            extraClass: \"o-mail-Chatter-dropzone\",\n            /** @param {Event} ev */\n            onDrop: async (ev) => {\n                if (this.state.composerType) {\n                    return;\n                }\n                if (isDragSourceExternalFile(ev.dataTransfer)) {\n                    const files = [...ev.dataTransfer.files];\n                    if (!this.state.thread.id) {\n                        const saved = await this.props.saveRecord?.();\n                        if (!saved) {\n                            return;\n                        }\n                    }\n                    Promise.all(files.map((file) => this.attachmentUploader.uploadFile(file))).then(\n                        () => {\n                            if (this.props.hasParentReloadOnAttachmentsChanged) {\n                                this.reloadParentView();\n                            }\n                        }\n                    );\n                    this.state.isAttachmentBoxOpened = true;\n                }\n            }\n        });\n        useEffect(\n            () => {\n                if (!this.state.thread) {\n                    return;\n                }\n                browser.clearTimeout(this.loadingAttachmentTimeout);\n                if (this.state.thread?.isLoadingAttachments) {\n                    this.loadingAttachmentTimeout = browser.setTimeout(\n                        () => (this.state.showAttachmentLoading = true),\n                        DELAY_FOR_SPINNER\n                    );\n                } else {\n                    this.state.showAttachmentLoading = false;\n                    this.state.isAttachmentBoxOpened =\n                        this.state.isAttachmentBoxOpened ||\n                        (this.props.isAttachmentBoxVisibleInitially && this.attachments.length > 0);\n                }\n                return () => browser.clearTimeout(this.loadingAttachmentTimeout);\n            },\n            () => [this.state.thread, this.state.thread?.isLoadingAttachments]\n        );\n        useEffect(\n            () => {\n                if (\n                    this.state.thread &&\n                    ![\"new\", \"loading\"].includes(this.state.thread.status) &&\n                    this.attachments.length === 0\n                ) {\n                    this.state.isAttachmentBoxOpened = false;\n                }\n            },\n            () => [this.state.thread?.status, this.attachments]\n        );\n        useEffect(\n            () => {\n                this.state.aside = this.props.isChatterAside;\n            },\n            () => [this.props.isChatterAside]\n        );\n    },\n\n    /**\n     * @returns {import(\"models\").Activity[]}\n     */\n    get activities() {\n        return this.state.thread?.activities ?? [];\n    },\n\n    get afterPostRequestList() {\n        return [\n            ...super.afterPostRequestList,\n            \"followers\",\n            \"scheduledMessages\",\n            \"suggestedRecipients\",\n        ];\n    },\n\n    get attachments() {\n        return this.state.thread?.attachments ?? [];\n    },\n\n    get childSubEnv() {\n        const res = Object.assign(super.childSubEnv, { messageHighlight: this.messageHighlight });\n        res.inChatter.aside = this.props.isChatterAside;\n        return res;\n    },\n\n    get followerButtonLabel() {\n        return _t(\"Show Followers\");\n    },\n\n    get followingText() {\n        return _t(\"Following\");\n    },\n\n    /**\n     * @returns {boolean}\n     */\n    get isDisabled() {\n        return !this.state.thread.id || !this.state.thread?.hasReadAccess;\n    },\n\n    get onCloseFullComposerRequestList() {\n        return [...super.onCloseFullComposerRequestList, \"scheduledMessages\"];\n    },\n\n    get requestList() {\n        return [\n            ...super.requestList,\n            \"activities\",\n            \"attachments\",\n            \"followers\",\n            \"scheduledMessages\",\n            \"suggestedRecipients\",\n        ];\n    },\n\n    get scheduledMessages() {\n        return this.state.thread?.scheduledMessages ?? [];\n    },\n\n    get unfollowText() {\n        return _t(\"Unfollow\");\n    },\n\n    changeThread(threadModel, threadId) {\n        super.changeThread(...arguments);\n        this.attachmentUploader.thread = this.state.thread;\n        if (threadId === false) {\n            this.state.composerType = false;\n        } else {\n            this.onThreadCreated?.(this.state.thread);\n            this.onThreadCreated = null;\n            this.messageSearch.thread = this.state.thread;\n            this.closeSearch();\n        }\n    },\n\n    closeSearch() {\n        this.messageSearch.clear();\n        this.state.isSearchOpen = false;\n    },\n\n    async _follow(thread) {\n        await this.orm.call(thread.model, \"message_subscribe\", [[thread.id]], {\n            partner_ids: [this.store.self.id],\n        });\n        this.onFollowerChanged(thread);\n    },\n\n    onActivityChanged(thread) {\n        this.load(thread, [...this.requestList, \"messages\"]);\n    },\n\n    onAddFollowers() {\n        this.load(this.state.thread, [\"followers\", \"suggestedRecipients\"]);\n        if (this.props.hasParentReloadOnFollowersUpdate) {\n            this.reloadParentView();\n        }\n    },\n\n    onClickAddAttachments() {\n        if (this.attachments.length === 0) {\n            return;\n        }\n        this.state.isAttachmentBoxOpened = !this.state.isAttachmentBoxOpened;\n        if (this.state.isAttachmentBoxOpened) {\n            this.rootRef.el.scrollTop = 0;\n            this.state.thread.scrollTop = \"bottom\";\n        }\n    },\n\n    async onClickAttachFile(ev) {\n        if (this.state.thread.id) {\n            return;\n        }\n        const saved = await this.props.saveRecord?.();\n        if (!saved) {\n            return false;\n        }\n    },\n\n    async onClickFollow() {\n        if (this.state.thread.id) {\n            this._follow(this.state.thread);\n        } else {\n            this.onThreadCreated = this._follow;\n            await this.props.saveRecord?.();\n        }\n    },\n\n    onClickSearch() {\n        this.state.composerType = false;\n        this.state.isSearchOpen = !this.state.isSearchOpen;\n    },\n\n    async onClickUnfollow() {\n        const thread = this.state.thread;\n        await thread.selfFollower.remove();\n        this.onFollowerChanged(thread);\n    },\n\n    onCloseFullComposerCallback() {\n        this.toggleComposer();\n        super.onCloseFullComposerCallback();\n    },\n\n    onFollowerChanged(thread) {\n        document.body.click(); // hack to close dropdown\n        this.reloadParentView();\n        this.load(thread, [\"followers\", \"suggestedRecipients\"]);\n    },\n\n    _onMounted() {\n        super._onMounted();\n        if (this.state.thread && this.props.highlightMessageId) {\n            this.state.thread.highlightMessage = this.props.highlightMessageId;\n        }\n    },\n\n    onPostCallback() {\n        if (this.props.hasParentReloadOnMessagePosted) {\n            this.reloadParentView();\n        }\n        this.toggleComposer();\n        super.onPostCallback();\n    },\n\n    onScheduledMessageChanged(thread) {\n        // reload messages as well as a scheduled message could have been sent\n        this.load(thread, [\"scheduledMessages\", \"messages\"]);\n    },\n\n    onSuggestedRecipientAdded(thread) {\n        this.load(thread, [\"suggestedRecipients\"]);\n    },\n\n    async onUploaded(data) {\n        await this.attachmentUploader.uploadData(data);\n        if (this.props.hasParentReloadOnAttachmentsChanged) {\n            this.reloadParentView();\n        }\n        this.state.isAttachmentBoxOpened = true;\n        if (this.rootRef.el) {\n            this.rootRef.el.scrollTop = 0;\n        }\n        this.state.thread.scrollTop = \"bottom\";\n    },\n\n    async reloadParentView() {\n        await this.props.saveRecord?.();\n        if (this.props.webRecord) {\n            await this.props.webRecord.load();\n        }\n    },\n\n    async scheduleActivity() {\n        this.closeSearch();\n        const schedule = async (thread) => {\n            await this.store.scheduleActivity(thread.model, [thread.id]);\n            this.load(thread, [\"activities\", \"messages\"]);\n        };\n        if (this.state.thread.id) {\n            schedule(this.state.thread);\n        } else {\n            this.onThreadCreated = schedule;\n            this.props.saveRecord?.();\n        }\n    },\n\n    toggleActivities() {\n        this.state.showActivities = !this.state.showActivities;\n    },\n\n    toggleComposer(mode = false) {\n        this.closeSearch();\n        const toggle = () => {\n            if (this.state.composerType === mode) {\n                this.state.composerType = false;\n            } else {\n                this.state.composerType = mode;\n            }\n        };\n        if (this.state.thread.id) {\n            toggle();\n        } else {\n            this.onThreadCreated = toggle;\n            this.props.saveRecord?.();\n        }\n    },\n\n    toggleScheduledMessages() {\n        this.state.showScheduledMessages = !this.state.showScheduledMessages;\n    },\n\n    async unlinkAttachment(attachment) {\n        await this.attachmentUploader.unlink(attachment);\n        if (this.props.hasParentReloadOnAttachmentsChanged) {\n            this.reloadParentView();\n        }\n    },\n\n    popoutAttachment() {\n        this.attachmentPopout.popout();\n    },\n});\n", "import { patch } from \"@web/core/utils/patch\";\nimport { FormArchParser } from \"@web/views/form/form_arch_parser\";\n\npatch(FormArchParser.prototype, {\n    parse(xmlDoc, models, modelName) {\n        const result = super.parse(...arguments);\n        result.has_activities = Boolean(models[modelName].has_activities);\n        return result;\n    },\n});\n", "import { registry } from \"@web/core/registry\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { append, createElement, extractAttributes, setAttributes } from \"@web/core/utils/xml\";\nimport { FormCompiler } from \"@web/views/form/form_compiler\";\n\n/** @this {FormCompiler} */\nfunction compileChatter(node, params) {\n    const chatterContainerXml = createElement(\"t\");\n    setAttributes(chatterContainerXml, {\n        \"t-component\": \"__comp__.mailComponents.Chatter\",\n        has_activities: \"__comp__.props.archInfo.has_activities\",\n        hasAttachmentPreview: Boolean(\n            this.templates.FormRenderer.querySelector(\".o_attachment_preview\")\n        ),\n        hasParentReloadOnAttachmentsChanged: Boolean(node.getAttribute(\"reload_on_attachment\")),\n        hasParentReloadOnFollowersUpdate: Boolean(node.getAttribute(\"reload_on_follower\")),\n        hasParentReloadOnMessagePosted: Boolean(node.getAttribute(\"reload_on_post\")),\n        isAttachmentBoxVisibleInitially: Boolean(node.getAttribute(\"open_attachments\")),\n        threadId: \"__comp__.props.record.resId or undefined\",\n        threadModel: \"__comp__.props.record.resModel\",\n        webRecord: \"__comp__.props.record\",\n        saveRecord: \"() => __comp__.save and __comp__.save()\",\n        highlightMessageId: \"__comp__.highlightMessageId\",\n    });\n    const chatterContainerHookXml = createElement(\"div\");\n    chatterContainerHookXml.classList.add(\"o-mail-ChatterContainer\", \"o-mail-Form-chatter\");\n    setAttributes(chatterContainerHookXml, { \"t-if\": \"!__comp__.env.inDialog\" });\n    append(chatterContainerHookXml, chatterContainerXml);\n    return chatterContainerHookXml;\n}\n\nfunction compileAttachmentPreview(node, params) {\n    const webClientViewAttachmentViewContainerHookXml = createElement(\"div\");\n    webClientViewAttachmentViewContainerHookXml.classList.add(\"o_attachment_preview\");\n    const webClientViewAttachmentViewContainerXml = createElement(\"t\");\n    setAttributes(webClientViewAttachmentViewContainerXml, {\n        \"t-component\": \"__comp__.mailComponents.AttachmentView\",\n        threadId: \"__comp__.props.record.resId or undefined\",\n        threadModel: \"__comp__.props.record.resModel\",\n    });\n    append(webClientViewAttachmentViewContainerHookXml, webClientViewAttachmentViewContainerXml);\n    return webClientViewAttachmentViewContainerHookXml;\n}\n\nregistry.category(\"form_compilers\").add(\"chatter_compiler\", {\n    selector: \"chatter\",\n    fn: compileChatter,\n});\n\nregistry.category(\"form_compilers\").add(\"attachment_preview_compiler\", {\n    selector: \"div.o_attachment_preview\",\n    fn: compileAttachmentPreview,\n});\n\npatch(FormCompiler.prototype, {\n    compile(node, params) {\n        const res = super.compile(node, params);\n        const chatterContainerHookXml = res.querySelector(\".o-mail-Form-chatter\");\n        if (!chatterContainerHookXml) {\n            return res; // no chatter, keep the result as it is\n        }\n        const chatterContainerXml = chatterContainerHookXml.querySelector(\n            \"t[t-component='__comp__.mailComponents.Chatter']\"\n        );\n        setAttributes(chatterContainerXml, {\n            isChatterAside: \"false\",\n            isInFormSheetBg: \"false\",\n            saveRecord: \"__comp__.props.saveRecord\",\n        });\n        if (chatterContainerHookXml.parentNode.classList.contains(\"o_form_sheet\")) {\n            return res; // if chatter is inside sheet, keep it there\n        }\n        const formSheetBgXml = res.querySelector(\".o_form_sheet_bg\");\n        const parentXml = formSheetBgXml && formSheetBgXml.parentNode;\n        if (!parentXml) {\n            return res; // miss-config: a sheet-bg is required for the rest\n        }\n\n        const webClientViewAttachmentViewHookXml = res.querySelector(\".o_attachment_preview\");\n        const hasPreview = !!webClientViewAttachmentViewHookXml;\n        if (webClientViewAttachmentViewHookXml) {\n            // in sheet bg (attachment viewer present)\n            setAttributes(webClientViewAttachmentViewHookXml, {\n                \"t-if\": `__comp__.mailLayout(${hasPreview}).includes(\"COMBO\")`,\n            });\n            const sheetBgChatterContainerHookXml = chatterContainerHookXml.cloneNode(true);\n            sheetBgChatterContainerHookXml.classList.add(\"o-isInFormSheetBg\", \"w-auto\");\n            setAttributes(sheetBgChatterContainerHookXml, {\n                \"t-if\": `__comp__.mailLayout(${hasPreview}) == \"COMBO\"`,\n            });\n            append(formSheetBgXml, sheetBgChatterContainerHookXml);\n            const sheetBgChatterContainerXml = sheetBgChatterContainerHookXml.querySelector(\n                \"t[t-component='__comp__.mailComponents.Chatter']\"\n            );\n            setAttributes(sheetBgChatterContainerXml, {\n                isInFormSheetBg: \"true\",\n                isChatterAside: \"false\",\n            });\n        }\n        // after sheet bg (standard position, either aside or below)\n        setAttributes(chatterContainerXml, {\n            isInFormSheetBg: `[\"COMBO\", \"BOTTOM_CHATTER\"].includes(__comp__.mailLayout(${hasPreview}))`,\n            isChatterAside: `[\"SIDE_CHATTER\", \"EXTERNAL_COMBO_XXL\", \"EXTERNAL_COMBO\"].includes(__comp__.mailLayout(${hasPreview}))`,\n        });\n        const { [\"t-if\"]: tIf } = extractAttributes(chatterContainerHookXml, [\"t-if\"]);\n        setAttributes(chatterContainerHookXml, {\n            \"t-if\": `${\n                tIf ? tIf : \"true\"\n            } and (![\"COMBO\", \"NONE\"].includes(__comp__.mailLayout(${hasPreview})))`, // opposite of sheetBgChatterContainerHookXml\n            \"t-attf-class\": `{{ [\"SIDE_CHATTER\", \"EXTERNAL_COMBO_XXL\"].includes(__comp__.mailLayout(${hasPreview})) ? \"o-aside w-print-100\" : \"mt-4 mt-md-0\" }}`,\n        });\n        append(parentXml, chatterContainerHookXml);\n        return res;\n    },\n});\n", "import { session } from '@web/session';\nimport { patch } from '@web/core/utils/patch';\nimport { append, createElement, setAttributes } from '@web/core/utils/xml';\n\nimport {FormCompiler} from '@web/views/form/form_compiler';\n\npatch(FormCompiler.prototype, {\n    compile(node, params) {\n        const res = super.compile(node, params);\n        const chatterContainerHookXml = res.querySelector(\n            '.o_form_renderer > .o-mail-Form-chatter'\n        );\n        if (!chatterContainerHookXml) {\n            return res;\n        }\n        setAttributes(chatterContainerHookXml, {\n            't-ref': 'chatterContainer',\n        });\n        if (session.chatter_position === 'bottom') {\n            const formSheetBgXml = res.querySelector('.o_form_sheet_bg');\n            if (!chatterContainerHookXml || !formSheetBgXml?.parentNode) {\n            \treturn res;\n            }\n            const webClientViewAttachmentViewHookXml = res.querySelector(\n            \t'.o_attachment_preview'\n            );\n            const chatterContainerXml = chatterContainerHookXml.querySelector(\n                \"t[t-component='__comp__.mailComponents.Chatter']\"\n            );\n            const sheetBgChatterContainerHookXml = chatterContainerHookXml.cloneNode(true);\n            const sheetBgChatterContainerXml = sheetBgChatterContainerHookXml.querySelector(\n                \"t[t-component='__comp__.mailComponents.Chatter']\"\n            );\n            sheetBgChatterContainerHookXml.classList.add('o-isInFormSheetBg', 'w-auto');\n            append(formSheetBgXml, sheetBgChatterContainerHookXml);\n            setAttributes(sheetBgChatterContainerXml, {\n                isInFormSheetBg: 'true',\n                isChatterAside: 'false',\n            });\n            setAttributes(chatterContainerXml, {\n                isInFormSheetBg: 'true',\n                isChatterAside: 'false',\n            });\n            setAttributes(chatterContainerHookXml, {\n                't-if': 'false',\n            });\n            if (webClientViewAttachmentViewHookXml) {\n                setAttributes(webClientViewAttachmentViewHookXml, {\n                    't-if': 'false',\n                });\n            }\n        } else {\n            setAttributes(chatterContainerHookXml, {\n                't-att-style': '__comp__.chatterState.width ? `width: ${__comp__.chatterState.width}px;` : \"\"',\n            });\n            const chatterContainerResizeHookXml = createElement('span');\n            chatterContainerResizeHookXml.classList.add('mk_chatter_resize');\n            setAttributes(chatterContainerResizeHookXml, {\n                't-on-mousedown.stop.prevent': '__comp__.onStartChatterResize.bind(__comp__)',\n                't-on-dblclick.stop.prevent': '__comp__.onDoubleClickChatterResize.bind(__comp__)',\n            });\n            append(chatterContainerHookXml, chatterContainerResizeHookXml);\n        }\n        return res;\n    },\n});\n", "import { createDocumentFragmentFromContent } from \"@mail/utils/common/html\";\n\nimport { useSubEnv } from \"@odoo/owl\";\n\nimport { x2ManyCommands } from \"@web/core/orm_service\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { FormController } from \"@web/views/form/form_controller\";\n\npatch(FormController.prototype, {\n    setup() {\n        super.setup(...arguments);\n        if (this.env.services[\"mail.store\"]) {\n            this.mailStore = useService(\"mail.store\");\n        }\n        useSubEnv({\n            chatter: {\n                fetchData: true,\n                fetchMessages: true,\n            },\n        });\n    },\n    onWillLoadRoot(nextConfiguration) {\n        super.onWillLoadRoot(...arguments);\n        this.env.chatter.fetchData = true;\n        this.env.chatter.fetchMessages = true;\n        const isSameThread =\n            this.model.root?.resId === nextConfiguration.resId &&\n            this.model.root?.resModel === nextConfiguration.resModel;\n        if (isSameThread) {\n            // not first load\n            const { resModel, resId } = this.model.root;\n            this.env.bus.trigger(\"MAIL:RELOAD-THREAD\", { model: resModel, id: resId });\n        }\n    },\n\n    async onWillSaveRecord(record, changes) {\n        if (record.resModel === \"mail.compose.message\") {\n            const doc = createDocumentFragmentFromContent(changes.body);\n            const partnerElements = doc.querySelectorAll('[data-oe-model=\"res.partner\"]');\n            const partnerIds = Array.from(partnerElements).map((element) =>\n                parseInt(element.dataset.oeId)\n            );\n            if (partnerIds.length) {\n                if (changes.partner_ids[0] && changes.partner_ids[0][0] === x2ManyCommands.SET) {\n                    partnerIds.push(...changes.partner_ids[0][2]);\n                }\n                changes.partner_ids.push(...partnerIds.map((pid) => x2ManyCommands.link(pid)));\n            }\n        }\n    },\n});\n", "import { AttachmentView } from \"@mail/core/common/attachment_view\";\nimport { Chatter } from \"@mail/chatter/web_portal/chatter\";\n\nimport { onMounted, onWillUnmount, useState } from \"@odoo/owl\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { router } from \"@web/core/browser/router\";\nimport { SIZES } from \"@web/core/ui/ui_service\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { useDebounced } from \"@web/core/utils/timing\";\nimport { FormRenderer } from \"@web/views/form/form_renderer\";\n\npatch(FormRenderer.prototype, {\n    setup() {\n        super.setup();\n        this.mailComponents = {\n            AttachmentView,\n            Chatter,\n        };\n        this.highlightMessageId = router.current.highlight_message_id;\n        this.messagingState = useState({\n            /** @type {import(\"models\").Thread} */\n            thread: undefined,\n        });\n        if (this.env.services[\"mail.store\"]) {\n            this.mailStore = useService(\"mail.store\");\n        }\n        this.uiService = useService(\"ui\");\n        this.mailPopoutService = useService(\"mail.popout\");\n\n        this.onResize = useDebounced(this.render, 200);\n        onMounted(() => browser.addEventListener(\"resize\", this.onResize));\n        onWillUnmount(() => browser.removeEventListener(\"resize\", this.onResize));\n    },\n    /**\n     * @returns {boolean}\n     */\n    hasFile() {\n        if (!this.mailStore || !this.props.record.resId) {\n            return false;\n        }\n        this.messagingState.thread = this.mailStore.Thread.insert({\n            id: this.props.record.resId,\n            model: this.props.record.resModel,\n        });\n        return this.messagingState.thread.attachmentsInWebClientView.length > 0;\n    },\n    mailLayout(hasAttachmentContainer) {\n        const xxl = this.uiService.size >= SIZES.XXL;\n        const hasFile = this.hasFile();\n        const hasChatter = !!this.mailStore;\n        const hasExternalWindow = !!this.mailPopoutService.externalWindow;\n        if (hasExternalWindow && hasFile && hasAttachmentContainer) {\n            if (xxl) {\n                return \"EXTERNAL_COMBO_XXL\"; // chatter on the side, attachment in separate tab\n            }\n            return \"EXTERNAL_COMBO\"; // chatter on the bottom, attachment in separate tab\n        }\n        if (hasChatter) {\n            if (xxl) {\n                if (hasAttachmentContainer && hasFile) {\n                    return \"COMBO\"; // chatter on the bottom, attachment on the side\n                }\n                return \"SIDE_CHATTER\"; // chatter on the side, no attachment\n            }\n            return \"BOTTOM_CHATTER\"; // chatter on the bottom, no attachment\n        }\n        return \"NONE\";\n    },\n});\n", "import { formView } from \"@web/views/form/form_view\";\nimport { registry } from \"@web/core/registry\";\nimport { EventBus, toRaw, useEffect, useRef, useSubEnv } from \"@odoo/owl\";\nimport { useCustomDropzone } from \"@web/core/dropzone/dropzone_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useX2ManyCrud } from \"@web/views/fields/relational_utils\";\nimport { MailAttachmentDropzone } from \"@mail/core/common/mail_attachment_dropzone\";\n\nexport class MailComposerFormController extends formView.Controller {\n    static props = {\n        ...formView.Controller.props,\n        fullComposerBus: { type: EventBus, optional: true },\n    };\n    setup() {\n        super.setup();\n        toRaw(this.env.dialogData).model = \"mail.compose.message\";\n        if (this.props.fullComposerBus) {\n            useSubEnv({\n                fullComposerBus: this.props.fullComposerBus,\n            });\n        }\n    }\n}\n\nexport class MailComposerFormRenderer extends formView.Renderer {\n    setup() {\n        super.setup();\n        // Autofocus the visible editor in edition mode.\n        this.root = useRef(\"compiled_view_root\");\n        useEffect((isInEdition, root) => {\n            if (root && root.el && isInEdition) {\n                const element = root.el.querySelector(\".note-editable[contenteditable]\");\n                if (element) {\n                    element.focus();\n                    document.dispatchEvent(new Event(\"selectionchange\", {}));\n                }\n            }\n        }, () => [\n            this.props.record.isInEdition,\n            this.root,\n            this.props.record.resId\n        ]);\n\n        // Add file dropzone on full mail composer:\n        this.attachmentUploadService = useService(\"mail.attachment_upload\");\n        this.operations = useX2ManyCrud(() => {\n            return this.props.record.data[\"attachment_ids\"];\n        }, true);\n\n        useCustomDropzone(this.root, MailAttachmentDropzone, {\n            /** @param {Event} event */\n            onDrop: async event => {\n                const resIds = JSON.parse(this.props.record.data.res_ids);\n                const thread = await this.mailStore.Thread.insert({\n                    model: this.props.record.data.model,\n                    id: resIds[0],\n                });\n                for (const file of event.dataTransfer.files) {\n                    const attachment = await this.attachmentUploadService.upload(thread, thread.composer, file);\n                    await this.operations.saveRecord([attachment.id]);\n                }\n            }\n        });\n    }\n}\n\nregistry.category(\"views\").add(\"mail_composer_form\", {\n    ...formView,\n    Controller: MailComposerFormController,\n    Renderer: MailComposerFormRenderer,\n});\n", "import { DateTimeInput } from \"@web/core/datetime/datetime_input\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { serializeDateTime, today } from \"@web/core/l10n/dates\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class MailComposerScheduleDialog extends Component {\n    static template = \"mail.MailComposerScheduleDialog\";\n    static props = {\n        close: Function,\n        isNote: Boolean,\n        schedule: Function,\n    };\n    static components = {\n        DateTimeInput,\n        Dialog,\n    };\n\n    setup() {\n        const now = luxon.DateTime.now();\n        this.state = useState({\n            customDateTime: now\n                .plus({ hours: 1 })\n                .set({ minutes: Math.ceil(now.minute / 5) * 5, seconds: 0, milliseconds: 0 }),\n            selectedOption: \"morning\",\n        });\n        this.dateTimeFormat = {\n            day: \"numeric\",\n            hour: \"numeric\",\n            minute: \"numeric\",\n            month: \"short\",\n        };\n    }\n\n    get dateTimePickerProps() {\n        return {\n            minDate: luxon.DateTime.now(),\n            onSelect: (value) => (this.state.customDateTime = value),\n            type: \"datetime\",\n            value: this.state.customDateTime,\n        };\n    }\n\n    get mondayMorning() {\n        return today()\n            .plus({ days: (1 - today().weekday + 7) % 7 || 7 })\n            .set({ hour: 8 });\n    }\n\n    get tomorrowAfternoon() {\n        return today().plus({ days: 1 }).set({ hour: 13 });\n    }\n\n    get tomorrowMorning() {\n        return today().plus({ days: 1 }).set({ hour: 8 });\n    }\n\n    get scheduledDate() {\n        if (this.state.selectedOption === \"morning\") {\n            return this.tomorrowMorning;\n        } else if (this.state.selectedOption === \"afternoon\") {\n            return this.tomorrowAfternoon;\n        } else if (this.state.selectedOption === \"monday\") {\n            return this.mondayMorning;\n        } else {\n            return this.state.customDateTime;\n        }\n    }\n\n    async schedule() {\n        await this.props.schedule(serializeDateTime(this.scheduledDate));\n        this.props.close();\n    }\n}\n", "import { MailComposerScheduleDialog } from \"@mail/chatter/web/mail_composer_schedule_dialog\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nclass MailComposerSendDropdown extends Component {\n    static components = {\n        Dropdown,\n        DropdownItem,\n    };\n    static props = standardWidgetProps;\n    static template = \"mail.MailComposerSendDropdown\";\n\n    setup() {\n        super.setup();\n        this.actionService = useService(\"action\");\n        this.dialogService = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n        this.buttonState = useState({ disabled: false });\n    }\n\n    async onClickSend() {\n        this.buttonState.disabled = true;\n        // don't send message if save failed (eg. missing required field )\n        if (await this.props.record.save()) {\n            // schedule the message if a scheduled_date is set on the composer\n            // (when using a template with a scheduled_date on it)\n            const method = this.props.record.data.scheduled_date\n                ? \"action_schedule_message\"\n                : \"action_send_mail\";\n            this.actionService.doAction(\n                await this.orm.call(\"mail.compose.message\", method, [this.props.record.resId], {\n                    context: this.props.record.context,\n                }),\n            );\n        }\n        this.buttonState.disabled = false;\n    }\n\n    async onClickSendLater() {\n        // don't open dialog if save failed (eg. missing required field)\n        if (await this.props.record.save()) {\n            this.dialogService.add(MailComposerScheduleDialog, {\n                isNote: this.props.record.data.subtype_is_log,\n                schedule: async (scheduledDate) => {\n                    await this.env.services.action.doAction(\n                        await this.env.services.orm.call(\n                            \"mail.compose.message\",\n                            \"action_schedule_message\",\n                            [this.props.record.resId, scheduledDate],\n                            { context: this.props.record.context },\n                        ),\n                    );\n                },\n            });\n        }\n    }\n}\n\nconst mailComposerSendDropdown = { component: MailComposerSendDropdown };\n\nregistry.category(\"view_widgets\").add(\"mail_composer_send_dropdown\", mailComposerSendDropdown);\n", "import { AttachmentList } from \"@mail/core/common/attachment_list\";\nimport { RelativeTime } from \"@mail/core/common/relative_time\";\nimport { AvatarCardPopover } from \"@mail/discuss/web/avatar_card/avatar_card_popover\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nexport const SCHEDULED_MESSAGE_TRUNCATE_THRESHOLD = 50; // arbitrary, ~ 1 line on large screen\n\nexport class ScheduledMessage extends Component {\n    static props = {\n        onScheduledMessageChanged: Function,\n        scheduledMessage: Object,\n    };\n    static template = \"mail.ScheduledMessage\";\n    static components = {\n        AttachmentList,\n        RelativeTime,\n    };\n\n    setup() {\n        super.setup();\n        this.state = useState({\n            readMore: false,\n        });\n        this.avatarCard = usePopover(AvatarCardPopover);\n        this.dialogService = useService(\"dialog\");\n    }\n\n    get isShort() {\n        return (\n            this.props.scheduledMessage.textContent.length < SCHEDULED_MESSAGE_TRUNCATE_THRESHOLD\n        );\n    }\n\n    get scheduledDate() {\n        return this.props.scheduledMessage.scheduled_date.toLocaleString(\n            luxon.DateTime.DATETIME_SHORT,\n        );\n    }\n\n    get truncatedMessage() {\n        return (\n            this.props.scheduledMessage.textContent.substring(\n                0,\n                SCHEDULED_MESSAGE_TRUNCATE_THRESHOLD,\n            ) + \"...\"\n        );\n    }\n\n    async cancel() {\n        const thread = this.props.scheduledMessage.thread;\n        await this.props.scheduledMessage.cancel();\n        this.props.onScheduledMessageChanged(thread);\n    }\n\n    onClick(ev) {\n        this.props.scheduledMessage.store.handleClickOnLink(ev, this.props.scheduledMessage.thread);\n    }\n\n    async onClickAttachmentUnlink(attachment) {\n        attachment.remove();\n    }\n\n    onClickAuthor(ev) {\n        if (!this.avatarCard.isOpen) {\n            this.avatarCard.open(ev.currentTarget, {\n                id: this.props.scheduledMessage.author.userId,\n            });\n        }\n    }\n\n    onClickCancel() {\n        this.dialogService.add(ConfirmationDialog, {\n            body: _t(\"Are you sure you want to cancel the scheduled message?\"),\n            cancel: () => {},\n            cancelLabel: _t(\"Close\"),\n            confirm: this.cancel.bind(this),\n            confirmLabel: _t(\"Cancel Message\"),\n        });\n    }\n\n    async onClickEdit() {\n        await this.props.scheduledMessage.edit();\n        this.props.onScheduledMessageChanged(this.props.scheduledMessage.thread);\n    }\n\n    async onClickSendNow() {\n        await this.props.scheduledMessage.send();\n        this.props.onScheduledMessageChanged(this.props.scheduledMessage.thread);\n    }\n}\n", "import { Record } from \"@mail/core/common/record\";\nimport { htmlToTextContentInline } from \"@mail/utils/common/format\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class ScheduledMessage extends Record {\n    static id = \"id\";\n    /** @type {Object.<number, import(\"models\").ScheduledMessage>} */\n    static records = {};\n    /** @returns {import(\"models\").ScheduledMessage} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @type {number} */\n    id;\n    attachment_ids = Record.many(\"Attachment\");\n    author = Record.one(\"Persona\");\n    body = Record.attr(\"\", { html: true });\n    /** @type {boolean} */\n    composition_batch;\n    /** @type {luxon.DateTime} */\n    scheduled_date = Record.attr(undefined, { type: \"datetime\" });\n    /** @type {boolean} */\n    is_note;\n    textContent = Record.attr(false, {\n        compute() {\n            if (!this.body) {\n                return \"\";\n            }\n            return htmlToTextContentInline(this.body);\n        },\n    });\n    thread = Record.one(\"Thread\");\n    // Editors of the records can delete scheduled messages\n    get deletable() {\n        return this.store.self.isAdmin || this.thread.hasWriteAccess;\n    }\n\n    get editable() {\n        return this.store.self.isAdmin || this.isSelfAuthored;\n    }\n\n    get isSelfAuthored() {\n        return this.author.eq(this.store.self);\n    }\n\n    get isSubjectThreadName() {\n        return this.thread.name?.trim().toLowerCase() === this.subject?.trim().toLowerCase();\n    }\n\n    /**\n     * Cancel the scheduled message.\n     */\n    async cancel() {\n        await this.store.env.services.orm.unlink(\"mail.scheduled.message\", [this.id]);\n        this.delete();\n    }\n\n    /**\n     * Open the mail_compose_mesage form view to allow edition of the scheduled message.\n     * If the message has already been sent, displays a notification instead.\n     */\n    async edit() {\n        let action;\n        try {\n            action = await this.store.env.services.orm.call(\n                \"mail.scheduled.message\",\n                \"open_edit_form\",\n                [this.id],\n            );\n        } catch {\n            this.notifyAlreadySent();\n            return;\n        }\n        return new Promise((resolve) =>\n            this.store.env.services.action.doAction(action, { onClose: resolve }),\n        );\n    }\n\n    notifyAlreadySent() {\n        this.store.env.services.notification.add(_t(\"This message has already been sent.\"), {\n            type: \"warning\",\n        });\n    }\n\n    /**\n     * Send the scheduled message directly\n     */\n    async send() {\n        try {\n            await this.store.env.services.orm.call(\"mail.scheduled.message\", \"post_message\", [this.id]);\n        } catch {\n            // already sent (by someone else or by cron)\n            return;\n        }\n    }\n}\n\nScheduledMessage.register();\n", "import { Record } from \"@mail/core/common/record\";\nimport { Thread } from \"@mail/core/common/thread_model\";\nimport \"@mail/chatter/web_portal/thread_model_patch\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Thread.prototype, {\n    setup() {\n        super.setup();\n        this.scheduledMessages = Record.many(\"ScheduledMessage\", {\n            sort: (a, b) => {\n                if (a.scheduled_date === b.scheduled_date) {\n                    return a.id - b.id;\n                }\n                return a.scheduled_date < b.scheduled_date ? -1 : 1;\n            },\n            inverse: \"thread\",\n        });\n    },\n\n    /** @param {string[]} requestList */\n    async fetchData(requestList) {\n        this.isLoadingAttachments =\n            this.isLoadingAttachments || requestList.includes(\"attachments\");\n        await super.fetchData(requestList);\n        if (!this.mainAttachment && this.attachmentsInWebClientView.length > 0) {\n            this.setMainAttachmentFromIndex(0);\n        }\n    },\n});\n", "import { Component } from \"@odoo/owl\";\n\nimport { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nclass ActivityException extends Component {\n    static props = standardFieldProps;\n    static template = \"mail.ActivityException\";\n    static fieldDependencies = [{ name: \"activity_exception_icon\", type: \"char\" }];\n\n    get textClass() {\n        if (this.props.record.data[this.props.name]) {\n            return (\n                \"text-\" +\n                this.props.record.data[this.props.name] +\n                \" fa \" +\n                this.props.record.data.activity_exception_icon\n            );\n        }\n        return undefined;\n    }\n}\n\nObject.assign(ActivityException, {\n    props: standardFieldProps,\n    template: \"mail.ActivityException\",\n});\n\nregistry.category(\"fields\").add(\"activity_exception\", {\n    component: ActivityException,\n    fieldDependencies: ActivityException.fieldDependencies,\n    label: false,\n});\n", "import { useComponent } from \"@odoo/owl\";\n\nimport { useCommand } from \"@web/core/commands/command_hook\";\nimport { Domain } from \"@web/core/domain\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { getFieldDomain } from \"@web/model/relational_model/utils\";\n\n/**\n * Use this hook to add \"Assign to..\" and \"Assign/Unassign me\" to the command palette.\n */\n\nexport function useAssignUserCommand() {\n    const component = useComponent();\n    const orm = useService(\"orm\");\n    const type = component.props.record.fields[component.props.name].type;\n    if (component.relation !== \"res.users\") {\n        return;\n    }\n\n    const getCurrentIds = () => {\n        if (type === \"many2one\" && component.props.record.data[component.props.name]) {\n            return [component.props.record.data[component.props.name][0]];\n        } else if (type === \"many2many\") {\n            return component.props.record.data[component.props.name].currentIds;\n        }\n        return [];\n    };\n\n    const add = async (record) => {\n        if (type === \"many2one\") {\n            component.props.record.update({ [component.props.name]: record });\n        } else if (type === \"many2many\") {\n            component.props.record.data[component.props.name].linkTo(record[0], {\n                display_name: record[1],\n            });\n        }\n    };\n\n    const remove = async (record) => {\n        if (type === \"many2one\") {\n            component.props.record.update({ [component.props.name]: false });\n        } else if (type === \"many2many\") {\n            component.props.record.data[component.props.name].unlinkFrom(record[0]);\n        }\n    };\n\n    const provide = async (env, options) => {\n        const value = options.searchValue.trim();\n        let domain = getFieldDomain(\n            component.props.record,\n            component.props.name,\n            component.props.domain\n        );\n        const context = component.props.context;\n        if (type === \"many2many\") {\n            const selectedUserIds = getCurrentIds();\n            if (selectedUserIds.length) {\n                domain = Domain.and([domain, [[\"id\", \"not in\", selectedUserIds]]]).toList();\n            }\n        }\n        component._pendingRpc?.abort(false);\n        component._pendingRpc = orm.call(component.relation, \"name_search\", [], {\n            name: value,\n            args: domain,\n            operator: \"ilike\",\n            limit: 80,\n            context,\n        });\n        const searchResult = await component._pendingRpc;\n        component._pendingRpc = null;\n        return searchResult.map((record) => ({\n            name: record[1],\n            action: add.bind(null, record),\n        }));\n    };\n    const options = {\n        category: \"smart_action\",\n        global: true,\n        identifier: component.props.string,\n    };\n    if (component.props.record.id !== component.props.record.model.root.id) {\n        // Only List View\n        options.isAvailable = () =>\n            component.props.record.model.multiEdit && component.props.record.selected;\n    } else {\n        options.isAvailable = () => true;\n    }\n    useCommand(\n        _t(\"Assign to ...\"),\n        () => ({\n            configByNameSpace: {\n                default: {\n                    emptyMessage: _t(\"No users found\"),\n                },\n            },\n            placeholder: _t(\"Select a user...\"),\n            providers: [\n                {\n                    provide,\n                },\n            ],\n        }),\n        {\n            ...options,\n            hotkey: \"alt+i\",\n        }\n    );\n\n    useCommand(\n        _t(\"Assign to me\"),\n        () => {\n            add([user.userId, user.name]);\n        },\n        {\n            ...options,\n            isAvailable: () => options.isAvailable() && !getCurrentIds().includes(user.userId),\n            hotkey: \"alt+shift+i\",\n        }\n    );\n    if (component.props.record.id === component.props.record.model.root.id) {\n        // Only Form View\n        useCommand(\n            _t(\"Unassign from me\"),\n            () => {\n                remove([user.userId, user.name]);\n            },\n            {\n                ...options,\n                isAvailable: () => options.isAvailable() && getCurrentIds().includes(user.userId),\n                hotkey: \"alt+shift+i\",\n            }\n        );\n    } else {\n        if (type === \"many2one\") {\n            useCommand(\n                _t(\"Unassign\"),\n                () => {\n                    remove([user.userId, user.name]);\n                },\n                {\n                    ...options,\n                    isAvailable: () => options.isAvailable() && getCurrentIds().length > 0,\n                    hotkey: \"alt+shift+u\",\n                }\n            );\n        } else {\n            useCommand(\n                _t(\"Unassign from me\"),\n                () => {\n                    remove([user.userId, user.name]);\n                },\n                {\n                    ...options,\n                    isAvailable: () =>\n                        options.isAvailable() && getCurrentIds().includes(user.userId),\n                    hotkey: \"alt+shift+u\",\n                }\n            );\n        }\n    }\n}\n", "import { usePopover } from \"@web/core/popover/popover_hook\";\nimport { AvatarCardPopover } from \"@mail/discuss/web/avatar_card/avatar_card_popover\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class Avatar extends Component {\n    static template = \"mail.Avatar\";\n    static props = {\n        resModel: { type: String },\n        resId: { type: Number },\n        displayName: { type: String },\n        noSpacing: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        this.avatarCard = usePopover(AvatarCardPopover);\n    }\n\n    onClickAvatar(ev) {\n        if (this.env.isSmall || !this.props.resId) {\n            return;\n        }\n        const target = ev.currentTarget;\n        if (!this.avatarCard.isOpen) {\n            this.avatarCard.open(target, {\n                id: this.props.resId,\n            });\n        }\n    }\n}\n", "import { EmojisFieldCommon } from \"@mail/views/web/fields/emojis_field_common/emojis_field_common\";\n\nimport { useRef } from \"@odoo/owl\";\n\nimport { registry } from \"@web/core/registry\";\nimport { CharField, charField } from \"@web/views/fields/char/char_field\";\n\n/**\n * Extension of the FieldChar that will add emojis support\n */\nexport class EmojisCharField extends EmojisFieldCommon(CharField) {\n    static template = \"mail.EmojisCharField\";\n    static components = { ...CharField.components };\n    setup() {\n        super.setup();\n        this.targetEditElement = useRef(\"input\");\n        this._setupOverride();\n    }\n\n    get shouldTrim() {\n        return false;\n    }\n}\n\nexport const emojisCharField = {\n    ...charField,\n    component: EmojisCharField,\n    additionalClasses: [...(charField.additionalClasses || []), \"o_field_text\"],\n};\n\nregistry.category(\"fields\").add(\"char_emojis\", emojisCharField);\n", "import { useEmojiPicker } from \"@web/core/emoji_picker/emoji_picker\";\n\nimport { useRef } from \"@odoo/owl\";\n\n/*\n * Common code for EmojisTextField and EmojisCharField\n */\nexport const EmojisFieldCommon = (T) =>\n    class EmojisFieldCommon extends T {\n        /**\n         * Create an emoji textfield view to enable opening an emoji popover\n         */\n        _setupOverride() {\n            this.emojiPicker = useEmojiPicker(\n                useRef(\"emojisButton\"),\n                {\n                    onSelect: (codepoints) => {\n                        const originalContent = this.targetEditElement.el.value;\n                        const start = this.targetEditElement.el.selectionStart;\n                        const end = this.targetEditElement.el.selectionEnd;\n                        const left = originalContent.slice(0, start);\n                        const right = originalContent.slice(end, originalContent.length);\n                        this.targetEditElement.el.value = left + codepoints + right;\n                        // trigger onInput from input_field hook to set field as dirty\n                        this.targetEditElement.el.dispatchEvent(new InputEvent(\"input\"));\n                        // keydown serves to both commit the changes in input_field and trigger onchange for some fields\n                        this.targetEditElement.el.dispatchEvent(new KeyboardEvent(\"keydown\"));\n                        this.targetEditElement.el.focus();\n                        const newCursorPos = start + codepoints.length;\n                        this.targetEditElement.el.setSelectionRange(newCursorPos, newCursorPos);\n                        if (this._emojiAdded) {\n                            this._emojiAdded();\n                        }\n                    },\n                },\n                {\n                    position: \"bottom\",\n                }\n            );\n        }\n    };\n", "import { EmojisFieldCommon } from \"@mail/views/web/fields/emojis_field_common/emojis_field_common\";\n\nimport { registry } from \"@web/core/registry\";\nimport { TextField, textField } from \"@web/views/fields/text/text_field\";\n\n/**\n * Extension of the FieldText that will add emojis support\n */\nexport class EmojisTextField extends EmojisFieldCommon(TextField) {\n    static template = \"mail.EmojisTextField\";\n    static components = { ...TextField.components };\n    setup() {\n        super.setup();\n        this.targetEditElement = this.textareaRef;\n        this._setupOverride();\n    }\n}\n\nexport const emojisTextField = {\n    ...textField,\n    component: EmojisTextField,\n    additionalClasses: [...(textField.additionalClasses || []), \"o_field_text\"],\n};\n\nregistry.category(\"fields\").add(\"text_emojis\", emojisTextField);\n", "import { DYNAMIC_PLACEHOLDER_PLUGINS } from \"@html_editor/plugin_sets\";\nimport { isEmpty } from \"@html_editor/utils/dom_info\";\nimport { registry } from \"@web/core/registry\";\nimport { useBus } from \"@web/core/utils/hooks\";\nimport { HtmlMailField, htmlMailField } from \"../html_mail_field/html_mail_field\";\nimport { MentionPlugin } from \"./mention_plugin\";\nimport { SIGNATURE_CLASS } from \"@html_editor/main/signature_plugin\";\n\nexport class HtmlComposerMessageField extends HtmlMailField {\n    setup() {\n        super.setup();\n        if (this.env.fullComposerBus) {\n            useBus(this.env.fullComposerBus, \"ACCIDENTAL_DISCARD\", (ev) => {\n                const elContent = this.getNoSignatureElContent();\n                ev.detail.onAccidentalDiscard(isEmpty(elContent));\n            });\n            useBus(this.env.fullComposerBus, \"SAVE_CONTENT\", (ev) => {\n                const emailAddSignature = Boolean(\n                    this.editor.editable.querySelector(`.${SIGNATURE_CLASS}`)\n                );\n                const elContent = this.getNoSignatureElContent();\n                // Temporarily Put the content in the DOM to be able to extract innerText newLines.\n                this.editor.editable.after(elContent);\n                // TODO: the following legacy regex may not have the desired effect as it\n                // agglomerates multiple newLines together.\n                const textValue = elContent.innerText.replace(/(\\t|\\n)+/g, \"\\n\");\n                elContent.remove();\n                ev.detail.onSaveContent(textValue, emailAddSignature);\n            });\n        }\n    }\n\n    getConfig() {\n        const config = super.getConfig(...arguments);\n        config.Plugins = [...config.Plugins, MentionPlugin];\n        if (!this.props.record.data.composition_batch) {\n            config.Plugins = config.Plugins.filter(\n                (plugin) => !DYNAMIC_PLACEHOLDER_PLUGINS.includes(plugin)\n            );\n        }\n        config.onAttachmentChange = (attachment) => {\n            // This only needs to happen for the composer for now\n            if (\n                !(\n                    this.props.record.fieldNames.includes(\"attachment_ids\") &&\n                    this.props.record.resModel === \"mail.compose.message\"\n                )\n            ) {\n                return;\n            }\n            this.props.record.data.attachment_ids.linkTo(attachment.id, attachment);\n        };\n        return config;\n    }\n\n    getNoSignatureElContent() {\n        const elContent = this.editor.getElContent();\n        this.editor.shared.signature.cleanSignatures({ rootClone: elContent });\n        return elContent;\n    }\n}\n\nexport const htmlComposerMessageField = {\n    ...htmlMailField,\n    component: HtmlComposerMessageField,\n};\n\nregistry.category(\"fields\").add(\"html_composer_message\", htmlComposerMessageField);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { MentionList } from \"@mail/core/web/mention_list\";\nimport { stateToUrl } from \"@web/core/browser/router\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { url } from \"@web/core/utils/urls\";\n\nexport class MentionPlugin extends Plugin {\n    static id = \"mention\";\n    static dependencies = [\"overlay\", \"dom\", \"history\", \"input\", \"selection\"];\n\n    resources = {\n        beforeinput_handlers: this.onBeforeInput.bind(this),\n    };\n\n    setup() {\n        this.mentionList = this.dependencies.overlay.createOverlay(MentionList, {\n            hasAutofocus: true,\n            className: \"popover\",\n        });\n    }\n\n    onSelect(ev, option) {\n        this.dependencies.selection.focusEditable();\n        const mentionBlock = renderToElement(\"mail.Wysiwyg.mentionLink\", {\n            option,\n            href: url(\n                stateToUrl({\n                    model: option.partner ? \"res.partner\" : \"discuss.channel\",\n                    resId: option.partner ? option.partner.id : option.channel.id,\n                })\n            ),\n        });\n        const nameNode = this.document.createTextNode(\n            `${option.partner ? \"@\" : \"#\"}${option.label}`\n        );\n        mentionBlock.appendChild(nameNode);\n        this.historySavePointRestore();\n        this.dependencies.dom.insert(mentionBlock);\n        this.dependencies.history.addStep();\n    }\n\n    onBeforeInput(ev) {\n        if (ev.data === \"@\" || ev.data === \"#\") {\n            this.historySavePointRestore = this.dependencies.history.makeSavePoint();\n            this.mentionList.open({\n                props: {\n                    onSelect: this.onSelect.bind(this),\n                    type: ev.data === \"@\" ? \"partner\" : \"channel\",\n                    close: () => {\n                        this.mentionList.close();\n                    },\n                },\n            });\n        }\n    }\n}\n", "import { isBlock } from \"@html_editor/utils/blocks\";\nimport { rgbToHex } from \"@html_editor/utils/color\";\nimport { getAdjacentPreviousSiblings } from \"@html_editor/utils/dom_traversal\";\n\nfunction 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\nfunction 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\n//--------------------------------------------------------------------------\n// Constants\n//--------------------------------------------------------------------------\n\nconst RE_COL_MATCH = /(^| )col(-[\\w\\d]+)*( |$)/;\nconst RE_COMMAS_OUTSIDE_PARENTHESES = /,(?![^(]*?\\))/g;\nconst RE_OFFSET_MATCH = /(^| )offset(-[\\w\\d]+)*( |$)/;\nconst RE_PADDING_MATCH = /[ ]*padding[^;]*;/g;\nconst RE_PADDING = /([\\d.]+)/;\nconst RE_WHITESPACE = /[\\s\\u200b]*/;\nconst SELECTORS_IGNORE = /(^\\*$|:hover|:before|:after|:active|:link|::|'|\\([^(),]+[,(])|@page/;\n// CSS properties relating to font, which Outlook seem to have trouble inheriting.\nconst FONT_PROPERTIES_TO_INHERIT = [\n    \"color\",\n    \"font-size\",\n    \"font-family\",\n    \"font-weight\",\n    \"font-style\",\n    \"text-decoration\",\n    \"text-transform\",\n    \"text-align\",\n];\n// Attributes all tables should have in a mailing.\nexport const TABLE_ATTRIBUTES = {\n    cellspacing: 0,\n    cellpadding: 0,\n    border: 0,\n    width: \"100%\",\n    align: \"center\",\n    role: \"presentation\",\n};\n// Cancel tables default styles.\nexport const TABLE_STYLES = {\n    \"border-collapse\": \"collapse\",\n    \"text-align\": \"inherit\",\n    \"font-size\": \"unset\",\n    \"line-height\": \"inherit\",\n};\n\nconst GROUPED_STYLES = {\n    border: [\n        \"border-top-width\",\n        \"border-right-width\",\n        \"border-bottom-width\",\n        \"border-left-width\",\n        \"border-top-style\",\n        \"border-right-style\",\n        \"border-bottom-style\",\n        \"border-left-style\",\n    ],\n    padding: [\"padding-top\", \"padding-bottom\", \"padding-left\", \"padding-right\"],\n    margin: [\"margin-top\", \"margin-bottom\", \"margin-left\", \"margin-right\"],\n    \"border-radius\": [\n        \"border-top-left-radius\",\n        \"border-top-right-radius\",\n        \"border-bottom-right-radius\",\n        \"border-bottom-left-radius\",\n    ],\n};\n\n//--------------------------------------------------------------------------\n// Public\n//--------------------------------------------------------------------------\n\n/**\n * Convert snippets and mailing bodies to tables.\n *\n * @param {HTMLElement} element\n */\nexport function addTables(element) {\n    for (const snippet of element.querySelectorAll(\".o_mail_snippet_general, .o_layout\")) {\n        // Convert all snippets and the mailing itself into table > tr > td\n        const table = _createTable(snippet.attributes);\n\n        const row = document.createElement(\"tr\");\n        let col = document.createElement(\"td\");\n        row.appendChild(col);\n        if (snippet.classList.contains(\"o_basic_theme\")) {\n            const div = document.createElement(\"div\");\n            div.classList.add(\"o_apple_wrapper_padding\");\n            col.appendChild(div);\n            col = div;\n            const style = document.createElement(\"style\");\n            // We create a nested media query because it's only supported by a\n            // handful of clients, including Apple Mail, and we actually only\n            // want this for Apple Mail.\n            const padding = \"34px\"; // This is what's needed to align the content with Apple Mail's header.\n            style.textContent =\n                `@media{@media{.o_basic_theme div.o_apple_wrapper_padding{padding:${snippet.style.padding};}}}` +\n                `@media(min-width:961px){@media{@media{.o_basic_theme div.o_apple_wrapper_padding{padding-left:${padding};}}}}`;\n            div.before(style);\n        }\n        table.appendChild(row);\n\n        for (const child of [...snippet.childNodes]) {\n            col.appendChild(child);\n        }\n        snippet.before(table);\n        snippet.remove();\n\n        // If snippet doesn't have a table as child, wrap its contents in one.\n        const childTables = [...col.children].filter((child) => child.nodeName === \"TABLE\");\n        if (!childTables.length) {\n            const tableB = _createTable();\n            const rowB = document.createElement(\"tr\");\n            const colB = document.createElement(\"td\");\n\n            rowB.appendChild(colB);\n            tableB.appendChild(rowB);\n            for (const child of [...col.childNodes]) {\n                colB.appendChild(child);\n            }\n            col.appendChild(tableB);\n        }\n    }\n}\n/**\n * Convert CSS display for attachment link to real image.\n * Without this post process, the display depends on the CSS and the picture\n * does not appear when we use the html without css (to send by email for e.g.)\n *\n * @param {HTMLElement} element\n */\nfunction attachmentThumbnailToLinkImg(element) {\n    const links = [\n        ...element.querySelectorAll(`a[href*=\"/web/content/\"][data-mimetype]:empty`),\n    ].filter((link) => RE_WHITESPACE.test(link.textContent));\n    for (const link of links) {\n        const image = document.createElement(\"img\");\n        image.setAttribute(\n            \"src\",\n            _getStylePropertyValue(link, \"background-image\").replace(/(^url\\(['\"])|(['\"]\\)$)/g, \"\")\n        );\n        // Note: will trigger layout thrashing.\n        image.setAttribute(\"height\", Math.max(1, _getHeight(link)));\n        image.setAttribute(\"width\", Math.max(1, _getWidth(link)));\n        link.prepend(image);\n    }\n}\n/**\n * Convert Bootstrap rows and columns to actual tables.\n *\n * Note: Because of the limited support of media queries in emails, this doesn't\n * support the mixing and matching of column options (e.g., \"col-4 col-sm-6\" and\n * \"col col-4\" aren't supported).\n *\n * @param {Element} element\n */\nexport function bootstrapToTable(element) {\n    // First give all rows in columns a separate container parent.\n    for (const rowInColumn of [...element.querySelectorAll(\".row\")].filter((row) =>\n        RE_COL_MATCH.test(row.parentElement.className)\n    )) {\n        const parentColumn = rowInColumn.parentElement;\n        const previous = rowInColumn.previousElementSibling;\n        if (previous && previous.classList.contains(\"o_fake_table\")) {\n            // If a container was already created there, append to it.\n            previous.append(rowInColumn);\n        } else {\n            _wrap(rowInColumn, \"div\", \"o_fake_table\");\n        }\n        // Bootstrap rows have negative left and right margins, which are not\n        // supported by GMail and Outlook. Add up the padding of the column with\n        // the negative margin of the row to get the correct padding.\n        const rowStyle = getComputedStyle(rowInColumn);\n        const columnStyle = getComputedStyle(parentColumn);\n        for (const side of [\"left\", \"right\"]) {\n            const negativeMargin = +rowStyle[`margin-${side}`].replace(\"px\", \"\");\n            const columnPadding = +columnStyle[`padding-${side}`].replace(\"px\", \"\");\n            if (negativeMargin < 0 && columnPadding >= Math.abs(negativeMargin)) {\n                parentColumn.style[`padding-${side}`] = `${columnPadding + negativeMargin}px`;\n                rowInColumn.style[`margin-${side}`] = 0;\n            }\n        }\n    }\n\n    // These containers from the mass mailing masonry snippet require full\n    // height contents, which is only possible if the table itself has a set\n    // height. We also need to restyle it because of the change in structure.\n    for (const masonryTopInnerContainer of element.querySelectorAll(\n        \".s_masonry_block > .container\"\n    )) {\n        masonryTopInnerContainer.style.setProperty(\"height\", \"100%\");\n    }\n    for (const masonryGrid of element.querySelectorAll(\".o_masonry_grid_container\")) {\n        masonryGrid.style.setProperty(\"padding\", 0);\n        for (const fakeTable of [...masonryGrid.children].filter((c) =>\n            c.classList.contains(\"o_fake_table\")\n        )) {\n            fakeTable.style.setProperty(\"height\", _getHeight(fakeTable) + \"px\");\n        }\n    }\n    for (const masonryRow of element.querySelectorAll(\n        \".o_masonry_grid_container > .o_fake_table > .row.h-100\"\n    )) {\n        masonryRow.style.removeProperty(\"height\");\n        masonryRow.parentElement.style.setProperty(\"height\", \"100%\");\n    }\n\n    const containers = element.querySelectorAll(\".container, .container-fluid, .o_fake_table\");\n    // Capture the widths of the containers before manipulating it.\n    for (const container of containers) {\n        container.setAttribute(\"o-temp-width\", _getWidth(container));\n    }\n    // Now convert all containers with rows to tables.\n    for (const container of [...containers].filter((n) =>\n        [...n.children].some((c) => c.classList.contains(\"row\"))\n    )) {\n        // The width of the table was stored in a temporary attribute. Fetch it\n        // for use in `_applyColspan` and remove the attribute at the end.\n        const containerWidth = parseFloat(container.getAttribute(\"o-temp-width\"));\n\n        // TABLE\n        const table = _createTable(container.attributes);\n        for (const child of [...container.childNodes]) {\n            table.append(child);\n        }\n        table.classList.remove(\"container\", \"container-fluid\", \"o_fake_table\");\n        if (!table.className) {\n            table.removeAttribute(\"class\");\n        }\n        container.before(table);\n        container.remove();\n\n        // ROWS\n        // First give all siblings of rows a separate row/col parent combo.\n        for (const row of [...table.children].filter(\n            (child) => isBlock(child) && !child.classList.contains(\"row\")\n        )) {\n            const newCol = _wrap(row, \"div\", \"col-12\");\n            _wrap(newCol, \"div\", \"row\");\n        }\n\n        for (const bootstrapRow of [...table.children].filter((c) => c.classList.contains(\"row\"))) {\n            const tr = document.createElement(\"tr\");\n            for (const attr of bootstrapRow.attributes) {\n                tr.setAttribute(attr.name, attr.value);\n            }\n            tr.classList.remove(\"row\");\n            if (!tr.className) {\n                tr.removeAttribute(\"class\");\n            }\n            for (const child of [...bootstrapRow.childNodes]) {\n                tr.append(child);\n            }\n            bootstrapRow.before(tr);\n            bootstrapRow.remove();\n\n            // COLUMNS\n            const bootstrapColumns = [...tr.children].filter(\n                (column) => column.className && column.className.match(RE_COL_MATCH)\n            );\n\n            // 1. Replace generic \"col\" classes with specific \"col-n\", computed\n            //    by sharing the available space between them.\n            const flexColumns = bootstrapColumns.filter(\n                (column) => !/\\d/.test(column.className.match(RE_COL_MATCH)[0] || \"0\")\n            );\n            const colTotalSize = bootstrapColumns\n                .map((child) => _getColumnSize(child) + _getColumnOffsetSize(child))\n                .reduce((a, b) => a + b, 0);\n            const colSize = Math.max(1, Math.round((12 - colTotalSize) / flexColumns.length));\n            for (const flexColumn of flexColumns) {\n                flexColumn.classList.remove(flexColumn.className.match(RE_COL_MATCH)[0].trim());\n                flexColumn.classList.add(`col-${colSize}`);\n            }\n\n            // 2. Create and fill up the row(s) with grid(s).\n            // Create new, empty columns for column offsets.\n            let columnIndex = 0;\n            for (const bootstrapColumn of [...bootstrapColumns]) {\n                const offsetSize = _getColumnOffsetSize(bootstrapColumn);\n                if (offsetSize) {\n                    const newColumn = document.createElement(\"div\");\n                    newColumn.classList.add(`col-${offsetSize}`);\n                    bootstrapColumn.classList.remove(\n                        bootstrapColumn.className.match(RE_OFFSET_MATCH)[0].trim()\n                    );\n                    bootstrapColumn.before(newColumn);\n                    bootstrapColumns.splice(columnIndex, 0, newColumn);\n                    columnIndex++;\n                }\n                columnIndex++;\n            }\n            let grid = _createColumnGrid();\n            let gridIndex = 0;\n            let currentRow = tr.cloneNode();\n            tr.after(currentRow);\n            let currentCol;\n            columnIndex = 0;\n            for (const bootstrapColumn of bootstrapColumns) {\n                const columnSize = _getColumnSize(bootstrapColumn);\n                if (gridIndex + columnSize < 12) {\n                    currentCol = grid[gridIndex];\n                    _applyColspan(currentCol, columnSize, containerWidth);\n                    gridIndex += columnSize;\n                    if (columnIndex === bootstrapColumns.length - 1) {\n                        // We handled all the columns but there is still space\n                        // in the row. Insert the columns and fill the row.\n                        _applyColspan(grid[gridIndex], 12 - gridIndex, containerWidth);\n                        currentRow.append(...grid.filter((td) => td.getAttribute(\"colspan\")));\n                    }\n                } else if (gridIndex + columnSize === 12) {\n                    // Finish the row.\n                    currentCol = grid[gridIndex];\n                    _applyColspan(currentCol, columnSize, containerWidth);\n                    currentRow.append(...grid.filter((td) => td.getAttribute(\"colspan\")));\n                    if (columnIndex !== bootstrapColumns.length - 1) {\n                        // The row was filled before we handled all of its\n                        // columns. Create a new one and start again from there.\n                        const previousRow = currentRow;\n                        currentRow = currentRow.cloneNode();\n                        previousRow.after(currentRow);\n                        grid = _createColumnGrid();\n                        gridIndex = 0;\n                    }\n                } else {\n                    // Fill the row with what was in the grid before it\n                    // overflowed.\n                    _applyColspan(grid[gridIndex], 12 - gridIndex, containerWidth);\n                    currentRow.append(...grid.filter((td) => td.getAttribute(\"colspan\")));\n                    // Start a new row that starts with the current col.\n                    const previousRow = currentRow;\n                    currentRow = currentRow.cloneNode();\n                    previousRow.after(currentRow);\n                    grid = _createColumnGrid();\n                    currentCol = grid[0];\n                    _applyColspan(currentCol, columnSize, containerWidth);\n                    gridIndex = columnSize;\n                    if (columnIndex === bootstrapColumns.length - 1 && gridIndex < 12) {\n                        // We handled all the columns but there is still space\n                        // in the row. Insert the columns and fill the row.\n                        _applyColspan(grid[gridIndex], 12 - gridIndex, containerWidth);\n                        currentRow.append(...grid.filter((td) => td.getAttribute(\"colspan\")));\n                    }\n                }\n                if (currentCol) {\n                    for (const attr of bootstrapColumn.attributes) {\n                        if (attr.name !== \"colspan\") {\n                            currentCol.setAttribute(attr.name, attr.value);\n                        }\n                    }\n                    const colMatch = bootstrapColumn.className.match(RE_COL_MATCH);\n                    currentCol.classList.remove(colMatch[0].trim());\n                    if (!currentCol.className) {\n                        currentCol.removeAttribute(\"class\");\n                    }\n                    for (const child of [...bootstrapColumn.childNodes]) {\n                        currentCol.append(child);\n                    }\n                    // Adapt width to colspan.\n                    _applyColspan(currentCol, +currentCol.getAttribute(\"colspan\"), containerWidth);\n                }\n                columnIndex++;\n            }\n            tr.remove(); // row was cloned and inserted already\n        }\n    }\n    for (const table of element.querySelectorAll(\"table\")) {\n        table.removeAttribute(\"o-temp-width\");\n    }\n    // Merge tables in tds into one common table, each in its own row.\n    const tds = [...element.querySelectorAll(\"td\")]\n        .filter(\n            (td) =>\n                td.children.length > 1 &&\n                [...td.children].every((child) => child.nodeName === \"TABLE\")\n        )\n        .reverse();\n    for (const td of tds) {\n        const table = _createTable();\n        const trs = [...td.children]\n            .map((child) => _wrap(child, \"td\"))\n            .map((wrappedChild) => _wrap(wrappedChild, \"tr\"));\n        trs[0].before(table);\n        table.append(...trs);\n    }\n}\n/**\n * Convert Bootstrap cards to table structures.\n *\n * @param {Element} element\n */\nexport function cardToTable(element) {\n    for (const card of element.querySelectorAll(\".card\")) {\n        const table = _createTable(card.attributes);\n        table.style.removeProperty(\"overflow\");\n        const cardImgTopSuperRows = [];\n        for (const child of [...card.childNodes]) {\n            const row = document.createElement(\"tr\");\n            const col = document.createElement(\"td\");\n            if (![\"IMG\", \"A\"].includes(child.nodeName) && isBlock(child)) {\n                for (const attr of child.attributes) {\n                    col.setAttribute(attr.name, attr.value);\n                }\n                for (const descendant of [...child.childNodes]) {\n                    col.append(descendant);\n                }\n                child.remove();\n            } else if (child.nodeType === Node.TEXT_NODE) {\n                if (child.textContent.replace(RE_WHITESPACE, \"\").length) {\n                    col.append(child);\n                } else {\n                    continue;\n                }\n            } else {\n                col.append(child);\n            }\n            const subTable = _createTable();\n            const superRow = document.createElement(\"tr\");\n            const superCol = document.createElement(\"td\");\n            row.append(col);\n            subTable.append(row);\n            superCol.append(subTable);\n            superRow.append(superCol);\n            table.append(superRow);\n            if (child.nodeType === Node.ELEMENT_NODE) {\n                const hasImgTop = [child, ...child.querySelectorAll(\".card-img-top\")].some(\n                    (node) =>\n                        node.classList &&\n                        node.classList.contains(\"card-img-top\") &&\n                        node.closest &&\n                        node.closest(\".card\") === table\n                );\n                if (hasImgTop) {\n                    // Collect .card-img-top superRows to manipulate their heights.\n                    cardImgTopSuperRows.push(superRow);\n                }\n            }\n        }\n        // We expect successive .card-img-top to have the same height so the\n        // bodies of the cards are aligned. This achieves that without flexboxes\n        // by forcing the height of the smallest card:\n        const smallestCardImgRow = Math.min(\n            0,\n            ...cardImgTopSuperRows.map((row) => row.clientHeight)\n        );\n        for (const row of cardImgTopSuperRows) {\n            row.style.height = smallestCardImgRow + \"px\";\n        }\n        card.before(table);\n        card.remove();\n    }\n}\n/**\n * Convert CSS style to inline style (leave the classes on elements but forces\n * the style they give as inline style).\n *\n * @param {HTMLElement} $element\n * @param {Object} cssRules\n */\nexport function classToStyle(element, cssRules) {\n    const writes = [];\n    const nodeToRules = new Map();\n    const rulesToProcess = [];\n    for (const rule of cssRules) {\n        const nodes = element.querySelectorAll(rule.selector);\n        if (nodes.length) {\n            rulesToProcess.push(rule);\n        }\n        for (const node of nodes) {\n            const nodeRules = nodeToRules.get(node);\n            if (!nodeRules) {\n                nodeToRules.set(node, [rule]);\n            } else {\n                nodeRules.push(rule);\n            }\n        }\n    }\n    _computeStyleAndSpecificityOnRules(rulesToProcess);\n    for (const rules of nodeToRules.values()) {\n        rules.sort((a, b) => a.specificity - b.specificity);\n    }\n\n    for (const node of nodeToRules.keys()) {\n        const nodeRules = nodeToRules.get(node);\n        const css = nodeRules ? _getMatchedCSSRules(node, nodeRules) : {};\n        // Flexbox\n        for (const styleName of node.style) {\n            if (styleName.includes(\"flex\") || `${node.style[styleName]}`.includes(\"flex\")) {\n                writes.push(() => {\n                    node.style[styleName] = \"\";\n                });\n            }\n        }\n\n        // Do not apply css that would override inline styles (which are prioritary).\n        let style = node.getAttribute(\"style\") || \"\";\n        // Outlook doesn't support inline !important\n        style = style.replace(/!important/g, \"\");\n        for (const [key, value] of Object.entries(css)) {\n            if (!new RegExp(`(^|;)\\\\s*${key}[ :]`).test(style)) {\n                style = `${key}:${value};${style}`;\n            }\n        }\n        if (Object.keys(style || {}).length === 0) {\n            writes.push(() => {\n                node.removeAttribute(\"style\");\n            });\n        } else {\n            writes.push(() => {\n                node.setAttribute(\"style\", style);\n                if (node.style.width) {\n                    node.setAttribute(\"width\", node.style.width.replace(\"px\", \"\").trim());\n                }\n            });\n        }\n\n        if (node.nodeName === \"IMG\") {\n            writes.push(() => {\n                // Media list images should not have an inline height\n                if (node.classList.contains(\"s_media_list_img\")) {\n                    node.style.removeProperty(\"height\");\n                }\n                // Protect aspect ratio when resizing in mobile.\n                if (\n                    node.style.getPropertyValue(\"width\") === \"100%\" &&\n                    node.style.getPropertyValue(\"object-fit\") === \"\"\n                ) {\n                    node.style.setProperty(\"object-fit\", \"cover\");\n                }\n            });\n        }\n        // Apple Mail\n        if (node.nodeName === \"TD\" && !node.childNodes.length) {\n            // Append non-breaking spaces to empty table cells.\n            writes.push(() => {\n                node.appendChild(document.createTextNode(\"\\u00A0\"));\n            });\n        }\n        // Outlook\n        if (\n            node.nodeName === \"A\" &&\n            node.classList.contains(\"btn\") &&\n            !node.classList.contains(\"btn-link\") &&\n            !node.children.length\n        ) {\n            writes.push(() => {\n                node.before(\n                    _createMso(`<table align=\"center\" border=\"0\"\n                    role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\"\n                    style=\"border-radius: 6px; border-collapse: separate !important;\">\n                        <tbody>\n                            <tr>\n                                <td style=\"${node.style.cssText\n                                    .replace(RE_PADDING_MATCH, \"\")\n                                    .replaceAll('\"', \"&quot;\")}\" ${\n                        node.parentElement.style.textAlign === \"center\" ? 'align=\"center\" ' : \"\"\n                    }bgcolor=\"${rgbToHex(node.style.backgroundColor)}\">\n                    `)\n                );\n                node.after(\n                    _createMso(`</td>\n                        </tr>\n                    </tbody>\n                </table>`)\n                );\n            });\n        } else if (\n            node.nodeName === \"IMG\" &&\n            node.classList.contains(\"mx-auto\") &&\n            node.classList.contains(\"d-block\")\n        ) {\n            writes.push(() => {\n                _wrap(node, \"p\", \"o_outlook_hack\", \"text-align:center;margin:0\");\n            });\n        }\n\n        // Compute dynamic styles (var, calc).\n        writes.push(() => {\n            let computedStyle;\n            for (const styleName of node.style) {\n                const styleValue = node.style.getPropertyValue(styleName);\n                if (styleValue.includes(\"var(\") || styleValue.includes(\"calc(\")) {\n                    computedStyle = computedStyle || getComputedStyle(node);\n                    const prop = styleValue.includes(\"var(\")\n                        ? styleValue.replace(/var\\((.*)\\)/, \"$1\")\n                        : styleName;\n                    const value =\n                        computedStyle.getPropertyValue(prop) ||\n                        computedStyle.getPropertyValue(styleName);\n                    node.style.setProperty(styleName, value);\n                }\n            }\n        });\n\n        // Fix inheritance of font properties on Outlook.\n        writes.push(() => {\n            const propsToConvert = FONT_PROPERTIES_TO_INHERIT.filter(\n                (prop) => node.style[prop] === \"inherit\"\n            );\n            if (propsToConvert.length) {\n                const computedStyle = getComputedStyle(node);\n                for (const prop of propsToConvert) {\n                    node.style.setProperty(prop, computedStyle[prop]);\n                }\n            }\n        });\n    }\n    writes.forEach((fn) => fn());\n}\n/**\n * Add styles to all table rows and columns, that are necessary for them to be\n * responsive. This only works if columns have a max-width so the styles are\n * only applied to columns where that is the case.\n *\n * @param {Element} element\n */\nfunction enforceTablesResponsivity(element) {\n    // Trying this: https://www.litmus.com/blog/mobile-responsive-email-stacking/\n    const trs = [...element.querySelectorAll(\".o_mail_wrapper tr\")]\n        .filter((tr) => [...tr.children].some((td) => td.classList.contains(\"o_converted_col\")))\n        .reverse();\n    for (const tr of trs) {\n        const commonTable = _createTable();\n        commonTable.style.height = \"100%\";\n        const commonTr = document.createElement(\"tr\");\n        const commonTd = document.createElement(\"td\");\n        commonTr.appendChild(commonTd);\n        commonTable.appendChild(commonTr);\n        const tds = [...tr.children].filter((child) => child.nodeName === \"TD\");\n        let index = 0;\n        for (const td of tds) {\n            const width = td.style.maxWidth;\n            const div = document.createElement(\"div\");\n            div.style.display = \"inline-block\";\n            div.style.verticalAlign = \"top\";\n            div.classList.add(\"o_stacking_wrapper\");\n            commonTd.appendChild(div);\n            const newTable = _createTable();\n            newTable.style.width = width;\n            newTable.classList.add(\"o_stacking_wrapper\");\n            div.appendChild(newTable);\n            const newTr = document.createElement(\"tr\");\n            newTable.appendChild(newTr);\n            newTr.appendChild(td);\n            td.style.width = \"100%\";\n            td.removeAttribute(\"width\");\n            if (index === 0) {\n                div.before(\n                    _createMso(`\n                    <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" role=\"presentation\" style=\"width: 100%;\">\n                        <tr>\n                            <td valign=\"top\" style=\"width: ${width};\">`)\n                );\n            } else {\n                div.before(_createMso(`</td><td valign=\"top\" style=\"width: ${width};\">`));\n            }\n            if (index === tds.length - 1) {\n                div.after(_createMso(`</td></tr></table>`));\n            }\n            index++;\n        }\n        const topTd = document.createElement(\"td\");\n        topTd.appendChild(commonTable);\n        tr.prepend(topTd);\n    }\n}\n// Masonry has crazy nested tables that require some extra treatment.\nfunction handleMasonry(element) {\n    const masonryTrs = element.querySelectorAll(\".s_masonry_block tr\");\n    for (const tr of masonryTrs) {\n        const height = _getHeight(tr);\n        const tds = [...tr.children].filter((child) => child.nodeName === \"TD\");\n        const tdsWithTable = tds.filter((td) =>\n            [...td.children].some((child) => child.nodeName === \"TABLE\")\n        );\n        if (tdsWithTable.length) {\n            // TODO: this seems a duplicate of the other o_desktop_h100 set below.\n            // Set the cells' heights to fill their parents.\n            for (const tdWithTable of tdsWithTable) {\n                tdWithTable.classList.add(\"o_desktop_h100\");\n                tdWithTable.style.setProperty(\"height\", \"100%\");\n            }\n            // We also have to set the same height on the cells' sibling TDs.\n            tds.forEach((td) => td.style.setProperty(\"height\", height + \"px\"));\n        }\n        // Sometimes Masonry declares rows with a height of 100% but with\n        // columns that overfit the grid. In these cases, we split the rows into\n        // multiple rows so we need to adapt their heights for them to be\n        // divided equally.\n        const trSiblings = [...tr.parentElement.children].filter(\n            (child) => child.nodeName === \"TR\"\n        );\n        if (\n            trSiblings.length > 1 &&\n            (tr.classList.contains(\"h-100\") || tr.style.getPropertyValue(\"height\") === \"100%\")\n        ) {\n            tr.style.setProperty(\"height\", `${_getHeight(tr.parentElement) / trSiblings.length}px`);\n        }\n    }\n    for (const tr of masonryTrs) {\n        const height = tr.style.height.includes(\"px\")\n            ? parseFloat(tr.style.height.replace(\"px\", \"\").trim())\n            : _getHeight(tr);\n        tr.closest(\"table\").classList.add(\"o_desktop_h100\");\n        tr.classList.add(\"o_desktop_h100\");\n        for (const td of [...tr.children].filter((child) => child.nodeName === \"TD\")) {\n            td.classList.add(\"o_desktop_h100\");\n            td.style.setProperty(\"height\", \"100%\");\n            const childrenNames = [...td.children].map((child) => child.nodeName);\n            if (!childrenNames.includes(\"TABLE\")) {\n                // Hack that makes vertical-align possible within an inline-block.\n                const wrapper = document.createElement(\"div\");\n                wrapper.style.setProperty(\"display\", \"inline-block\");\n                wrapper.style.setProperty(\"width\", \"100%\");\n                // Transfer color to wrapper for Outlook on MacOS/iOS.\n                const tdStyle = getComputedStyle(td);\n                wrapper.style.setProperty(\"color\", tdStyle.color);\n                const firstNonCommentChild = [...td.childNodes].find(\n                    (child) => child.nodeType !== Node.COMMENT_NODE\n                );\n                let anchor;\n                if (firstNonCommentChild) {\n                    anchor = getAdjacentPreviousSiblings(firstNonCommentChild)\n                        .filter((sib) => sib.nodeType !== Node.TEXT_NODE)\n                        .shift();\n                }\n                for (const child of [...td.childNodes].filter(\n                    (child) => child.nodeType !== Node.COMMENT_NODE\n                )) {\n                    wrapper.append(child);\n                }\n                anchor ? anchor.after(wrapper) : td.append(wrapper);\n                const centeringSpan = document.createElement(\"span\");\n                centeringSpan.style.setProperty(\"height\", \"100%\");\n                centeringSpan.style.setProperty(\"display\", \"inline-block\");\n                centeringSpan.style.setProperty(\"vertical-align\", \"middle\");\n                td.prepend(centeringSpan);\n                // Height on cells should be applied in pixels.\n                if (td.style.height.includes(\"%\")) {\n                    const newHeight =\n                        (height * parseFloat(td.style.height.replace(\"%\").trim())) / 100;\n                    td.style.setProperty(\"height\", newHeight + \"px\");\n                    // Spread height down for responsivity\n                    td.style.setProperty(\"max-height\", newHeight + \"px\");\n                    wrapper.style.setProperty(\"max-height\", newHeight + \"px\");\n                    if (\n                        wrapper.childElementCount === 1 &&\n                        wrapper.firstElementChild.nodeName === \"IMG\" &&\n                        wrapper.firstElementChild.style.height === \"100%\"\n                    ) {\n                        wrapper.firstElementChild.style.setProperty(\"max-height\", newHeight + \"px\");\n                    }\n                }\n            }\n        }\n    }\n}\n/**\n * Modify the styles of images so they are responsive.\n *\n * @param {Element} element\n */\nfunction enforceImagesResponsivity(element) {\n    // Images with 100% height in cells should preserve that height and the\n    // height of the row should be applied to the cell.\n    for (const image of element.querySelectorAll(\"td > img\")) {\n        const td = image.parentElement;\n        if (\n            td.childElementCount === 1 &&\n            (image.classList.contains(\"h-100\") ||\n                _getStylePropertyValue(image, \"height\") === \"100%\")\n        ) {\n            td.style.setProperty(\"height\", _getHeight(td.parentElement) + \"px\");\n            image.style.setProperty(\"height\", \"100%\");\n        }\n    }\n    // Remove the height attribute in card images so they can resize\n    // responsively, but leave it for Outlook.\n    for (const image of element.querySelectorAll('img[width=\"100%\"][height]')) {\n        image.before(_createMso(image.outerHTML));\n        image.classList.add(\"mso-hide\");\n        image.removeAttribute(\"height\");\n    }\n}\n/**\n * Convert the contents of an element area into content\n * that is widely compatible with email clients.\n *\n * @param {HTMLElement} element\n * @param {Object[]} cssRules Array<{selector: string;\n *                            style: {[styleName]: string};\n *                            specificity: number;}>\n */\nexport async function toInline(element, cssRules) {\n    // Fix card-img-top heights (must happen before we transform everything).\n    for (const imgTop of element.querySelectorAll(\".card-img-top\")) {\n        imgTop.style.setProperty(\"height\", _getHeight(imgTop) + \"px\");\n    }\n\n    attachmentThumbnailToLinkImg(element);\n    fontToImg(element);\n    await svgToPng(element);\n\n    // Fix img-fluid for Outlook.\n    for (const image of element.querySelectorAll(\"img.img-fluid\")) {\n        const width = _getWidth(image);\n        const clone = image.cloneNode();\n        clone.setAttribute(\"width\", width);\n        clone.style.setProperty(\"width\", width + \"px\");\n        clone.style.removeProperty(\"max-width\");\n        image.before(_createMso(clone.outerHTML));\n        _hideForOutlook(image);\n    }\n\n    classToStyle(element, cssRules);\n    bootstrapToTable(element);\n    cardToTable(element);\n    listGroupToTable(element);\n    addTables(element);\n    handleMasonry(element);\n    const rootFontSizeProperty = getComputedStyle(element.ownerDocument.documentElement).fontSize;\n    const rootFontSize = parseFloat(rootFontSizeProperty.replace(/[^\\d.]/g, \"\"));\n    normalizeRem(element, rootFontSize);\n    enforceImagesResponsivity(element);\n    enforceTablesResponsivity(element);\n    flattenBackgroundImages(element);\n    formatTables(element);\n    normalizeColors(element);\n    responsiveToStaticForOutlook(element);\n    // Fix Outlook image rendering bug.\n    for (const attributeName of [\"width\", \"height\"]) {\n        const images = element.querySelectorAll(\"img\");\n        for (const image of images) {\n            if (image.style[attributeName] !== \"auto\") {\n                const value =\n                    image.getAttribute(attributeName) ||\n                    (attributeName === \"height\" && image.offsetHeight) ||\n                    (attributeName === \"width\" ? _getWidth(image) : _getHeight(image));\n                if (value) {\n                    image.setAttribute(attributeName, value);\n                    image.style.setProperty(attributeName, value + \"px\");\n                }\n            }\n        }\n    }\n    // Fix mx-auto on images in table cells.\n    for (const centeredImage of element.querySelectorAll(\"td > img.mx-auto\")) {\n        if (centeredImage.parentElement.children.length === 1) {\n            centeredImage.parentElement.style.setProperty(\"text-align\", \"center\");\n        }\n    }\n\n    // Remove contenteditable attributes\n    [element, ...element.querySelectorAll(\"[contenteditable]\")].forEach((node) =>\n        node.removeAttribute(\"contenteditable\")\n    );\n\n    // Hide replaced cells on Outlook\n    element.querySelectorAll(\".mso-hide\").forEach(_hideForOutlook);\n\n    // Replace double quotes in font-family styles with simple quotes (and\n    // simply remove these styles from images).\n    element\n        .querySelectorAll(\"[style*=font-family]\")\n        .forEach((n) =>\n            n.nodeName === \"IMG\"\n                ? n.style.removeProperty(\"font-family\")\n                : n.setAttribute(\"style\", n.getAttribute(\"style\").replaceAll('\"', \"'\"))\n        );\n\n    element\n        .querySelectorAll(\".o_converted_col\")\n        .forEach((node) => node.classList.remove(\"o_converted_col\"));\n}\n/**\n * Take all elements with a `background-image` style and convert them to `vml`\n * for Outlook. Also remove data-bg-src to avoid Gmail cutting the html.\n *\n * @param {Element} element\n */\nfunction flattenBackgroundImages(element) {\n    const backgroundImages = [...element.querySelectorAll(\"*[style*=background-image]\")]\n        .filter((el) => !el.closest(\".mso-hide\"))\n        .reverse();\n    for (const backgroundImage of backgroundImages) {\n        const vml = _backgroundImageToVml(backgroundImage);\n        if (vml) {\n            // Put the Outlook version after the original one in an mso conditional.\n            backgroundImage.after(_createMso(vml));\n            // Hide the original element for Outlook.\n            backgroundImage.classList.add(\"mso-hide\");\n        }\n        if (backgroundImage.hasAttribute(\"data-bg-src\")) {\n            // Remove data-bg-src as it is not needed for email rendering and\n            // can cause Gmail to cut the email prematurely if the attributes\n            // contain an image in the form of a long base64 string.\n            backgroundImage.removeAttribute(\"data-bg-src\");\n        }\n    }\n}\n/**\n * Convert font icons to images.\n *\n * @param {HTMLElement} element - the element in which the font icons have to be\n *                           converted to images\n */\nfunction fontToImg(element) {\n    const { fonts } = odoo.loader.modules.get(\"@html_editor/utils/fonts\");\n\n    for (const font of element.querySelectorAll(\".fa\")) {\n        let icon, content;\n        fonts.fontIcons.find((fontIcon) => {\n            return fonts.getCssSelectors(fontIcon.parser).find((data) => {\n                if (font.matches(data.selector.replace(/::?before/g, \"\"))) {\n                    icon = data.names[0].split(\"-\").shift();\n                    content = data.css.match(/content:\\s*['\"]?(.)['\"]?/)[1];\n                    return true;\n                }\n            });\n        });\n        if (content) {\n            const color = _getStylePropertyValue(font, \"color\").replace(/\\s/g, \"\");\n            let backgroundColoredElement = font;\n            let bg, isTransparent;\n            do {\n                bg = _getStylePropertyValue(backgroundColoredElement, \"background-color\").replace(\n                    /\\s/g,\n                    \"\"\n                );\n                isTransparent = bg === \"transparent\" || bg === \"rgba(0,0,0,0)\";\n                backgroundColoredElement = backgroundColoredElement.parentElement;\n            } while (isTransparent && backgroundColoredElement);\n            if (bg === \"rgba(0,0,0,0)\" && isTransparent) {\n                // default on white rather than black background since opacity\n                // is not supported.\n                bg = \"rgb(255,255,255)\";\n            }\n            const style = font.getAttribute(\"style\");\n            const width = _getWidth(font);\n            const height = _getHeight(font);\n            const lineHeight = _getStylePropertyValue(font, \"line-height\");\n            // Compute the padding.\n            // First get the dimensions of the icon itself (::before)\n            font.style.setProperty(\"height\", \"fit-content\");\n            font.style.setProperty(\"width\", \"fit-content\");\n            font.style.setProperty(\"line-height\", \"normal\");\n            const intrinsicWidth = _getWidth(font);\n            const intrinsicHeight = _getHeight(font);\n            const hPadding = width && intrinsicWidth && (width - intrinsicWidth) / 2;\n            const vPadding = height && intrinsicHeight && (height - intrinsicHeight) / 2;\n            let padding = \"\";\n            if (hPadding || vPadding) {\n                padding = vPadding ? vPadding + \"px \" : \"0 \";\n                padding += hPadding ? hPadding + \"px\" : \"0\";\n            }\n            const image = document.createElement(\"img\");\n            image.setAttribute(\"width\", intrinsicWidth);\n            image.setAttribute(\"height\", intrinsicHeight);\n            // @todo @phoenix adapt controller to html_editor\n            image.setAttribute(\n                \"src\",\n                `/web_editor/font_to_img/${content.charCodeAt(0)}/${encodeURIComponent(\n                    color\n                )}/${encodeURIComponent(bg)}/${Math.max(1, Math.round(intrinsicWidth))}x${Math.max(\n                    1,\n                    Math.round(intrinsicHeight)\n                )}`\n            );\n            image.setAttribute(\"data-class\", font.getAttribute(\"class\"));\n            image.setAttribute(\"data-style\", style);\n            image.setAttribute(\"style\", style);\n            image.style.setProperty(\"box-sizing\", \"border-box\"); // keep the fontawesome's dimensions\n            image.style.setProperty(\"line-height\", lineHeight);\n            image.style.setProperty(\"width\", intrinsicWidth + \"px\");\n            image.style.setProperty(\"height\", intrinsicHeight + \"px\");\n            image.style.setProperty(\"vertical-align\", \"unset\"); // undo Bootstrap's default (middle).\n            if (!padding) {\n                image.style.setProperty(\"margin\", _getStylePropertyValue(font, \"margin\"));\n            }\n            // For rounded images, apply the rounded border to a wrapper, make\n            // sure it doesn't get applied to the image itself so the image\n            // doesn't get cropped in the process.\n            const wrapper = document.createElement(\"span\");\n            wrapper.style.setProperty(\"display\", \"inline-block\");\n            wrapper.append(image);\n            font.before(wrapper);\n            if (font.classList.contains(\"mx-auto\")) {\n                wrapper.parentElement.style.textAlign = \"center\";\n            }\n            font.remove();\n            wrapper.style.setProperty(\"padding\", padding);\n            const wrapperWidth =\n                width +\n                [\"left\", \"right\"].reduce(\n                    (sum, side) =>\n                        sum +\n                        (+_getStylePropertyValue(image, `margin-${side}`).replace(\"px\", \"\") || 0),\n                    0\n                );\n            wrapper.style.setProperty(\"width\", wrapperWidth + \"px\");\n            wrapper.style.setProperty(\"height\", height + \"px\");\n            wrapper.style.setProperty(\"vertical-align\", \"text-bottom\");\n            wrapper.style.setProperty(\"background-color\", image.style.backgroundColor);\n            wrapper.setAttribute(\n                \"class\",\n                \"oe_unbreakable \" + // prevent sanitize from grouping image wrappers\n                    font\n                        .getAttribute(\"class\")\n                        .replace(new RegExp(\"(^|\\\\s+)\" + icon + \"(-[^\\\\s]+)?\", \"gi\"), \"\") // remove inline font-awsome style\n            );\n        } else {\n            font.remove();\n        }\n    }\n}\n/**\n * Format table styles so they display well in most mail clients. This implies\n * moving table paddings to its cells, adding tbody (with canceled styles) where\n * needed, and adding pixel heights to parents of elements with percent heights.\n *\n * @param {HTMLElement} element\n */\nexport function formatTables(element) {\n    const writes = [];\n    for (const table of element.querySelectorAll(\n        \"table.o_mail_snippet_general, .o_mail_snippet_general table\"\n    )) {\n        const tablePaddingTop = parseFloat(\n            _getStylePropertyValue(table, \"padding-top\").match(RE_PADDING)[1]\n        );\n        const tablePaddingRight = parseFloat(\n            _getStylePropertyValue(table, \"padding-right\").match(RE_PADDING)[1]\n        );\n        const tablePaddingBottom = parseFloat(\n            _getStylePropertyValue(table, \"padding-bottom\").match(RE_PADDING)[1]\n        );\n        const tablePaddingLeft = parseFloat(\n            _getStylePropertyValue(table, \"padding-left\").match(RE_PADDING)[1]\n        );\n        const rows = [...table.querySelectorAll(\"tr\")].filter(\n            (tr) => tr.closest(\"table\") === table\n        );\n        const columns = [...table.querySelectorAll(\"td\")].filter(\n            (td) => td.closest(\"table\") === table\n        );\n        for (const column of columns) {\n            const columnsInRow = [...column.closest(\"tr\").querySelectorAll(\"td\")].filter(\n                (td) => td.closest(\"table\") === table\n            );\n            const columnIndex = columnsInRow.findIndex((col) => col === column);\n            const rowIndex = rows.findIndex((row) => row === column.closest(\"tr\"));\n\n            if (!rowIndex) {\n                const match = _getStylePropertyValue(column, \"padding-top\").match(RE_PADDING);\n                const columnPaddingTop = match ? parseFloat(match[1]) : 0;\n                writes.push(() => {\n                    column.style[\"padding-top\"] = `${columnPaddingTop + tablePaddingTop}px`;\n                });\n            }\n            if (columnIndex === columnsInRow.length - 1) {\n                const match = _getStylePropertyValue(column, \"padding-right\").match(RE_PADDING);\n                const columnPaddingRight = match ? parseFloat(match[1]) : 0;\n                writes.push(() => {\n                    column.style[\"padding-right\"] = `${columnPaddingRight + tablePaddingRight}px`;\n                });\n            }\n            if (rowIndex === rows.length - 1) {\n                const match = _getStylePropertyValue(column, \"padding-bottom\").match(RE_PADDING);\n                const columnPaddingBottom = match ? parseFloat(match[1]) : 0;\n                writes.push(() => {\n                    column.style[\"padding-bottom\"] = `${\n                        columnPaddingBottom + tablePaddingBottom\n                    }px`;\n                });\n            }\n            if (!columnIndex) {\n                const match = _getStylePropertyValue(column, \"padding-left\").match(RE_PADDING);\n                const columnPaddingLeft = match ? parseFloat(match[1]) : 0;\n                writes.push(() => {\n                    column.style[\"padding-left\"] = `${columnPaddingLeft + tablePaddingLeft}px`;\n                });\n            }\n        }\n        writes.push(() => {\n            table.style.removeProperty(\"padding\");\n        });\n    }\n    writes.forEach((fn) => fn());\n    // Ensure a tbody in every table and cancel its default style.\n    for (const table of [...element.querySelectorAll(\"table\")].filter(\n        (n) => ![...n.children].some((c) => c.nodeName === \"TBODY\")\n    )) {\n        const contents = [...table.childNodes];\n        const tbody = document.createElement(\"tbody\");\n        tbody.style.setProperty(\"vertical-align\", \"top\");\n        table.prepend(tbody);\n        tbody.append(...contents);\n    }\n    // Children will only take 100% height if the parent has a height property.\n    for (const node of [...element.querySelectorAll(\"*\")].filter(\n        (n) =>\n            n.style &&\n            n.style.getPropertyValue(\"height\") === \"100%\" &&\n            (!n.parentElement.style.getPropertyValue(\"height\") ||\n                n.parentElement.style.getPropertyValue(\"height\").includes(\"%\"))\n    )) {\n        let parent = node.parentElement;\n        let height = parent.style.getPropertyValue(\"height\");\n        while (parent && height && height.includes(\"%\")) {\n            parent = parent.parentElement;\n            height = parent.style.getPropertyValue(\"height\");\n        }\n        if (parent) {\n            parent.style.setProperty(\"height\", parent.getBoundingClientRect().height);\n        }\n    }\n    // Align self and justify content don't work on table cells.\n    for (const cell of element.querySelectorAll(\"td\")) {\n        const alignSelf = cell.style.alignSelf;\n        const justifyContent = cell.style.justifyContent;\n        if (\n            alignSelf === \"start\" ||\n            justifyContent === \"start\" ||\n            justifyContent === \"flex-start\"\n        ) {\n            cell.style.verticalAlign = \"top\";\n        } else if (alignSelf === \"center\" || justifyContent === \"center\") {\n            const parentCell = cell.parentElement.closest(\"td\");\n            const parentTable = cell.closest(\"table\");\n            if (parentCell) {\n                parentTable.style.height = _getHeight(parentCell) + \"px\";\n            }\n            cell.style.verticalAlign = \"middle\";\n        } else if (\n            alignSelf === \"end\" ||\n            justifyContent === \"end\" ||\n            justifyContent === \"flex-end\"\n        ) {\n            cell.style.verticalAlign = \"bottom\";\n        }\n    }\n    // Align items doesn't work on table rows.\n    for (const row of element.querySelectorAll(\"tr\")) {\n        const alignItems = row.style.alignItems;\n        if (alignItems === \"flex-start\") {\n            row.style.verticalAlign = \"top\";\n        } else if (alignItems === \"center\") {\n            row.style.verticalAlign = \"middle\";\n        } else if (alignItems === \"flex-end\" || alignItems === \"baseline\") {\n            row.style.verticalAlign = \"bottom\";\n        } else if (alignItems === \"stretch\") {\n            const columns = [...row.querySelectorAll(\"td.o_converted_col\")];\n            if (columns.length > 1) {\n                const commonAncestor = commonParentGet(columns[0], columns[1]);\n                const biggestHeight = commonAncestor.clientHeight;\n                for (const column of columns) {\n                    column.style.height = biggestHeight + \"px\";\n                }\n            }\n        }\n    }\n    // Tables don't properly inherit certain styles from their ancestors in Outlook.\n    for (const table of element.querySelectorAll(\"table\")) {\n        const propsToConvert = FONT_PROPERTIES_TO_INHERIT.filter(\n            (prop) => table.style[prop] === \"inherit\" || !table.style[prop]\n        );\n        if (propsToConvert.length) {\n            for (const prop of propsToConvert) {\n                let ancestor = table;\n                while (ancestor && (!ancestor.style[prop] || ancestor.style[prop] === \"inherit\")) {\n                    ancestor = ancestor.parentElement;\n                }\n                if (ancestor) {\n                    table.style.setProperty(prop, ancestor.style[prop]);\n                }\n            }\n        }\n    }\n}\n/**\n * Parse through the given document's stylesheets, preprocess(*) them and return\n * the result as an array of objects, each containing a selector string , a\n * style object and a specificity number. Preprocessing involves grouping\n * whatever rules can be grouped together and precomputing their specificity so\n * as to sort them appropriately.\n *\n * @param {Document} doc\n * @returns {Object[]} Array<{selector: string;\n *                            style: {[styleName]: string};\n *                            specificity: number;}>\n */\nexport function getCSSRules(doc) {\n    const cssRules = [];\n    for (const sheet of doc.styleSheets) {\n        // try...catch because browser may not able to enumerate rules for cross-domain sheets\n        let rules;\n        try {\n            rules = sheet.rules || sheet.cssRules;\n        } catch (e) {\n            console.log(\"Can't read the css rules of: \" + sheet.href, e);\n            continue;\n        }\n        for (const rule of rules || []) {\n            const subRules = [rule];\n            const conditionText = rule.conditionText;\n            const minWidthMatch = conditionText && conditionText.match(/\\(min-width *: *(\\d+)/);\n            const minWidth = minWidthMatch && +(minWidthMatch[1] || \"0\");\n            if (minWidth && minWidth >= 992) {\n                // Large min-width media queries should be included.\n                // eg., .container has a default max-width for all screens.\n                let mediaRules;\n                try {\n                    mediaRules = rule.rules || rule.cssRules;\n                    subRules.push(...mediaRules);\n                } catch (e) {\n                    console.log(`Can't read the css rules of: ${sheet.href} (${conditionText})`, e);\n                }\n            }\n            for (const subRule of subRules) {\n                const selectorText = subRule.selectorText || \"\";\n                // Split selectors, making sure not to split at commas in parentheses.\n                for (const selector of selectorText.split(RE_COMMAS_OUTSIDE_PARENTHESES)) {\n                    if (selector && !SELECTORS_IGNORE.test(selector)) {\n                        cssRules.push({ selector: selector.trim(), rawRule: subRule });\n                        if (selector === \"body\") {\n                            // The top element of a mailing has the class\n                            // 'o_layout'. Give it the body's styles so they can\n                            // trickle down.\n                            cssRules.push({\n                                selector: \".o_layout\",\n                                rawRule: subRule,\n                                specificity: 1,\n                            });\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    return cssRules;\n}\n/**\n * Convert Bootstrap list groups and their items to table structures.\n *\n * @param {Element} element\n */\nexport function listGroupToTable(element) {\n    for (const listGroup of element.querySelectorAll(\".list-group\")) {\n        let table;\n        if (listGroup.querySelectorAll(\".list-group-item\").length) {\n            table = _createTable(listGroup.attributes);\n        } else {\n            table = listGroup.cloneNode();\n            for (const attr of listGroup.attributes) {\n                table.setAttribute(attr.name, attr.value);\n            }\n        }\n        for (const child of [...listGroup.childNodes]) {\n            if (child.classList && child.classList.contains(\"list-group-item\")) {\n                // List groups are <ul>s that render like tables. Their\n                // li.list-group-item children should translate to tr > td.\n                const row = document.createElement(\"tr\");\n                const col = document.createElement(\"td\");\n                for (const attr of child.attributes) {\n                    col.setAttribute(attr.name, attr.value);\n                }\n                col.append(...child.childNodes);\n                col.classList.remove(\"list-group-item\");\n                if (!col.className) {\n                    col.removeAttribute(\"class\");\n                }\n                row.append(col);\n                table.append(row);\n                child.remove();\n            } else if (child.nodeName === \"LI\") {\n                table.append(...child.childNodes);\n            } else {\n                table.append(child);\n            }\n        }\n        table.classList.remove(\"list-group\");\n        if (!table.className) {\n            table.removeAttribute(\"class\");\n        }\n        if (listGroup.nodeName === \"TD\") {\n            listGroup.append(table);\n            listGroup.classList.remove(\"list-group\");\n            if (!listGroup.className) {\n                listGroup.removeAttribute(\"class\");\n            }\n        } else {\n            listGroup.before(table);\n            listGroup.remove();\n        }\n    }\n}\n/**\n * Convert all styles containing rgb colors to hexadecimal colors.\n * Note: ignores rgba colors, which are not supported in Microsoft Outlook.\n *\n * @param {HTMLElement} element\n */\nexport function normalizeColors(element) {\n    for (const node of element.querySelectorAll('[style*=\"rgb\"]')) {\n        const rgbaMatch = node.getAttribute(\"style\").match(/rgba?\\(([\\d.]+\\s*,?\\s*){3,4}\\)/g);\n        for (const rgb of rgbaMatch || []) {\n            node.setAttribute(\n                \"style\",\n                node.getAttribute(\"style\").replace(rgb, rgbToHex(rgb, node))\n            );\n        }\n    }\n}\n/**\n * Convert all css values that use the rem unit to px.\n *\n * @param {HTMLElement} $element\n * @param {Number} rootFontSize=16 The font size of the root element, in pixels\n */\nexport function normalizeRem(element, rootFontSize = 16) {\n    for (const node of element.querySelectorAll('[style*=\"rem\"]')) {\n        const remMatch = node.getAttribute(\"style\").match(/[\\d.]+\\s*rem/g);\n        for (const rem of remMatch || []) {\n            const remValue = parseFloat(rem.replace(/[^\\d.]/g, \"\"));\n            const pxValue = Math.round(remValue * rootFontSize * 100) / 100;\n            node.setAttribute(\"style\", node.getAttribute(\"style\").replace(rem, pxValue + \"px\"));\n        }\n    }\n}\n\n/**\n * This replaces column html with a dumbed down, Outlook-compliant version of\n * them just for Outlook so while not responsive, these columns still display OK\n * on Outlook.\n *\n * @param {Element} element\n */\nfunction responsiveToStaticForOutlook(element) {\n    // Replace the responsive tables with static ones for Outlook\n    for (const td of element.querySelectorAll(\"td.o_converted_col:not(.mso-hide)\")) {\n        const tdStyle = td.getAttribute(\"style\") || \"\";\n        const msoAttributes = [...td.attributes].filter(\n            (attr) => attr.name !== \"style\" && attr.name !== \"width\"\n        );\n        const msoWidth = td.style.getPropertyValue(\"max-width\");\n        const msoStyles = tdStyle.replace(/(^| |max-)width:[^;]*;\\s*/g, \"\");\n        const outlookTd = document.createElement(\"td\");\n        for (const attribute of msoAttributes) {\n            outlookTd.setAttribute(attribute.name, td.getAttribute(attribute.name));\n        }\n        if (msoWidth) {\n            outlookTd.setAttribute(\"width\", (\"\" + msoWidth).replace(\"px\", \"\").trim());\n            outlookTd.setAttribute(\"style\", `${msoStyles}width: ${msoWidth};`);\n        } else {\n            outlookTd.setAttribute(\"style\", msoStyles);\n        }\n        if (td.closest(\".s_masonry_block\")) {\n            outlookTd.style.padding = 0; // Not sure why this is needed.\n        }\n        // Outlook doesn't support left/right padding on images. When the image\n        // is the only child of its parent, apply said padding to the parent.\n        if (td.children.length === 1 && td.firstElementChild.nodeName === \"IMG\") {\n            const tdComputedStyle = getComputedStyle(td);\n            for (const side of [\"left\", \"right\"]) {\n                if (td.firstElementChild.style.width === \"100%\") {\n                    const prop = `padding-${side}`;\n                    const imagePadding = +td.firstElementChild.style[prop].replace(\"px\", \"\");\n                    if (imagePadding > 0) {\n                        const tdPadding = +tdComputedStyle[prop].replace(\"px\", \"\") || 0;\n                        outlookTd.style[prop] = tdPadding + imagePadding + \"px\";\n                    }\n                }\n            }\n        }\n        // The opening tag of `outlookTd` is for Outlook.\n        td.before(_createMso(outlookTd.outerHTML.replace(\"</td>\", \"\")));\n        // The opening tag of `td` is for the others.\n        _hideForOutlook(td, \"opening\");\n    }\n}\n/**\n * Convert images of type svg to png.\n *\n * @param {HTMLElement} element\n */\nasync function svgToPng(element) {\n    for (const svg of element.querySelectorAll('img[src*=\".svg\"]')) {\n        // Make sure the svg is loaded before we convert it.\n        await new Promise((resolve) => {\n            svg.onload = () => resolve();\n            if (svg.complete) {\n                resolve();\n            }\n        });\n        const image = document.createElement(\"img\");\n        const canvas = document.createElement(\"CANVAS\");\n        const width = _getWidth(svg);\n        const height = _getHeight(svg);\n\n        canvas.setAttribute(\"width\", width);\n        canvas.setAttribute(\"height\", height);\n        canvas.getContext(\"2d\").drawImage(svg, 0, 0, width, height);\n\n        for (const attribute of svg.attributes) {\n            image.setAttribute(attribute.name, attribute.value);\n        }\n\n        image.setAttribute(\"src\", canvas.toDataURL(\"png\"));\n        image.setAttribute(\"width\", width);\n        image.setAttribute(\"height\", height);\n\n        svg.before(image);\n        svg.remove();\n    }\n}\n\n//--------------------------------------------------------------------------\n// Private\n//--------------------------------------------------------------------------\n\n/**\n * Take an element and apply a colspan to it. In this context, this implies to\n * also apply a width to it, that corresponds to the colspan.\n *\n * @param {Element} element\n * @param {number} colspan\n * @param {number} tableWidth\n */\nfunction _applyColspan(element, colspan, tableWidth) {\n    element.setAttribute(\"colspan\", colspan);\n    const widthPercentage = +element.getAttribute(\"colspan\") / 12;\n    // Round to 2 decimal places.\n    const width = Math.round(tableWidth * widthPercentage * 100) / 100;\n    element.style.setProperty(\"max-width\", width + \"px\");\n    element.classList.add(\"o_converted_col\");\n}\n/**\n * Take an element with a background image and return a string containing the\n * VML code to display the same image properly in Outlook, with its contents\n * inside.\n * Note that this assumes:\n *   - background-size: cover,\n *   - background-repeat: no-repeat,\n *   - size 100%\n *   - content is centered x/y\n * TODO: centering span probably not needed with `v-text-anchor:middle` present.\n *\n * @param {Element} backgroundImage\n * @returns {string}\n */\nfunction _backgroundImageToVml(backgroundImage) {\n    const matches = backgroundImage.style.backgroundImage.match(/url\\(\"?(.+?)\"?\\)/);\n    const url = matches && matches[1];\n    if (url) {\n        // Create the outer structure.\n        const clone = backgroundImage.cloneNode(true);\n        const div = document.createElement(\"div\");\n        div.replaceChildren(...clone.childNodes);\n        [\n            [\"fontSize\", 0],\n            [\"height\", \"100%\"],\n            [\"width\", \"100%\"],\n        ].forEach(([k, v]) => (div.style[k] = v));\n        const vmlContent = document.createElement(\"div\");\n        vmlContent.append(div);\n\n        // Preserve important inherited properties without ancestor context.\n        const style = getComputedStyle(backgroundImage);\n        for (const prop of FONT_PROPERTIES_TO_INHERIT) {\n            div.style[prop] = backgroundImage.style[prop] || style[prop];\n        }\n        [...div.children].forEach((child) =>\n            child.style.setProperty(\"font-size\", child.style.fontSize || style.fontSize)\n        );\n\n        // Prepare the top element for hosting the VML image.\n        for (const prop of [\n            \"background\",\n            \"background-image\",\n            \"background-repeat\",\n            \"background-size\",\n        ]) {\n            clone.style.removeProperty(prop);\n        }\n        clone.style.padding = 0;\n        clone.className = clone.className.replace(/p[bt]\\d+/g, \"\"); // Remove padding classes.\n        clone.setAttribute(\"background\", url);\n        clone.setAttribute(\"valign\", \"middle\");\n\n        // Create the VML structure, with the content of the original element inside.\n        const [width, height] = [_getWidth(backgroundImage), _getHeight(backgroundImage)];\n        const vml =\n            `<v:image xmlns:v=\"urn:schemas-microsoft-com:vml\" fill=\"true\" stroke=\"false\" ` +\n            `style=\"border: 0; display: inline-block; width: ${width}px; height: ${height}px;\" src=\"${url}\"/>\n        <v:rect xmlns:v=\"urn:schemas-microsoft-com:vml\" fill=\"true\" stroke=\"false\" ` +\n            `style=\"border: 0; display: inline-block; position: absolute; width:${width}px; height:${height}px; v-text-anchor:middle;\">\n            <v:fill opacity=\"0%\" color=\"#000000\"/>\n            <v:textbox inset=\"0,0,0,0\">\n                <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\">\n                    <tr>\n                        <td width=\"${width}\" align=\"center\" style=\"text-align: center;\">${vmlContent.outerHTML}</td>\n                    </tr>\n                </table>\n            </v:textbox>\n        </v:rect>`;\n\n        // Wrap the VML in the original opening and closing tags.\n        return `${clone.outerHTML.replace(\n            /<\\/[\\w-]+>[\\s\\n]*$/,\n            \"\"\n        )}${vml}</${clone.nodeName.toLowerCase()}>`;\n    }\n}\n/**\n * Take a selector and return its specificity according to the w3 specification.\n *\n * @see http://www.w3.org/TR/css3-selectors/#specificity\n * @param {string} selector\n * @returns number\n */\nfunction _computeSpecificity(selector) {\n    let a = 0;\n    selector = selector.replace(/#[a-z0-9_-]+/gi, () => {\n        a++;\n        return \"\";\n    });\n    let b = 0;\n    selector = selector.replace(/(\\.[a-z0-9_-]+)|(\\[.*?\\])/gi, () => {\n        b++;\n        return \"\";\n    });\n    let c = 0;\n    selector = selector.replace(/(^|\\s+|:+)[a-z0-9_-]+/gi, (a) => {\n        if (!a.includes(\":not(\")) {\n            c++;\n        }\n        return \"\";\n    });\n    return a * 100 + b * 10 + c;\n}\n/**\n * Take all the rules and modify them to contain information on their\n * specificity and to have normalized style.\n *\n * @see _computeSpecificity\n * @see _normalizeStyle\n * @param {Object} cssRules\n */\nfunction _computeStyleAndSpecificityOnRules(cssRules) {\n    for (const cssRule of cssRules) {\n        if (!cssRule.style && cssRule.rawRule.style) {\n            const style = _normalizeStyle(cssRule.rawRule.style);\n            if (Object.keys(style).length) {\n                Object.assign(cssRule, {\n                    style,\n                    specificity: _computeSpecificity(cssRule.selector),\n                });\n            }\n        }\n    }\n}\n/**\n * Return an array of twelve table cells as JQuery elements.\n *\n * @returns {Element[]}\n */\nfunction _createColumnGrid() {\n    return new Array(12).fill().map(() => document.createElement(\"td\"));\n}\n/**\n * Return a comment element with the given content, wrapped in an mso condition.\n *\n * @param {string} content\n * @returns {Comment}\n */\nfunction _createMso(content = \"\") {\n    return document.createComment(`[if mso]>${content}<![endif]`);\n}\n/**\n * Return a table element, with its default styles and attributes, as well as\n * the applicable given attributes, if any.\n *\n * @see TABLE_ATTRIBUTES\n * @see TABLE_STYLES\n * @param {NamedNodeMap | Attr[]} [attributes] default: []\n * @returns {Element}\n */\nfunction _createTable(attributes = []) {\n    const table = document.createElement(\"table\");\n    Object.entries(TABLE_ATTRIBUTES).forEach(([att, value]) => table.setAttribute(att, value));\n    for (const attr of attributes) {\n        if (!(attr.name === \"width\" && attr.value === \"100%\")) {\n            table.setAttribute(attr.name, attr.value);\n        }\n    }\n    table.style.setProperty(\"width\", \"100%\", \"important\");\n    if (table.classList.contains(\"o_layout\")) {\n        // The top mailing element inherits the body's font size and line-height\n        // and should keep them.\n        const layoutStyles = { ...TABLE_STYLES };\n        delete layoutStyles[\"font-size\"];\n        delete layoutStyles[\"line-height\"];\n        Object.entries(layoutStyles).forEach(([att, value]) => (table.style[att] = value));\n    } else {\n        for (const styleName in TABLE_STYLES) {\n            if (!(\"style\" in attributes && attributes.style.value.includes(styleName + \":\"))) {\n                table.style[styleName] = TABLE_STYLES[styleName];\n            }\n        }\n    }\n    return table;\n}\n/**\n * Take a Bootstrap grid column element and return its size, computed by using\n * its Bootstrap classes.\n *\n * @see RE_COL_MATCH\n * @param {Element} column\n * @returns {number}\n */\nfunction _getColumnSize(column) {\n    const colMatch = column.className.match(RE_COL_MATCH);\n    const colOptions = colMatch[2] && colMatch[2].substr(1).split(\"-\");\n    const colSize =\n        (colOptions && (colOptions.length === 2 ? +colOptions[1] : +colOptions[0])) || 0;\n    return colSize;\n}\n/**\n * Take a Bootstrap grid column element and return its offset size, computed by\n * using its Bootstrap classes.\n *\n * @see RE_OFFSET_MATCH\n * @param {Element} column\n * @returns {number}\n */\nfunction _getColumnOffsetSize(column) {\n    const offsetMatch = column.className.match(RE_OFFSET_MATCH);\n    const offsetOptions = offsetMatch && offsetMatch[2] && offsetMatch[2].substr(1).split(\"-\");\n    const offsetSize =\n        (offsetOptions && (offsetOptions.length === 2 ? +offsetOptions[1] : +offsetOptions[0])) ||\n        0;\n    return offsetSize;\n}\n/**\n * Return the CSS rules which applies on an element, tweaked so that they are\n * browser/mail client ok.\n *\n * @param {Node} node\n * @param {Object[]} Array<{selector: string;\n *                          style: {[styleName]: string};\n *                          specificity: number;}>\n * @returns {Object} {[styleName]: string}\n */\nfunction _getMatchedCSSRules(node, cssRules) {\n    node.matches =\n        node.matches ||\n        node.webkitMatchesSelector ||\n        node.mozMatchesSelector ||\n        node.msMatchesSelector ||\n        node.oMatchesSelector;\n    const styles = cssRules.map((rule) => rule.style).filter(Boolean);\n\n    // Add inline styles at the highest specificity.\n    if (node.style.length) {\n        const inlineStyles = {};\n        for (const styleName of node.style) {\n            inlineStyles[styleName] = node.style[styleName];\n        }\n        styles.push(inlineStyles);\n    }\n\n    const processedStyle = {};\n    for (const style of styles) {\n        for (const [key, value] of Object.entries(style)) {\n            if (\n                !processedStyle[key] ||\n                !processedStyle[key].includes(\"important\") ||\n                value.includes(\"important\")\n            ) {\n                processedStyle[key] = value;\n            }\n        }\n    }\n\n    for (const [key, value] of Object.entries(processedStyle)) {\n        if (value && value.endsWith(\"important\")) {\n            processedStyle[key] = value.replace(/\\s*!important\\s*$/, \"\");\n        }\n    }\n\n    // When a grouped style (e.g., border-width, margin, padding) uses a CSS variable\n    // (e.g., var(--some-variable)), its substyles (e.g., margin-left, padding-top)\n    // won't have explicit values in CSSRule's style property. The grouped style itself\n    // also won't appear directly. To prevent losing these styles, we add the substyles\n    // explicitly using their computed values.\n    const computedStyle = getComputedStyle(node);\n    for (const groupName in GROUPED_STYLES) {\n        // We exclude the 'margin' and 'padding' styles from force apply because\n        // it's common that they have a value set by auto which doesn't make sense to\n        // force their computed value.\n        const force = !groupName.includes(\"margin\") && !groupName.includes(\"padding\");\n        const hasSubStyleApplied = GROUPED_STYLES[groupName].some(\n            (styleName) => styleName in processedStyle\n        );\n        if (!force && hasSubStyleApplied) {\n            continue;\n        }\n        for (const styleName of GROUPED_STYLES[groupName]) {\n            const styleValue = computedStyle.getPropertyValue(styleName);\n            if (styleValue && typeof styleValue === \"string\" && styleValue.length) {\n                processedStyle[styleName] = styleValue;\n            }\n        }\n    }\n\n    if (\n        processedStyle.display === \"block\" &&\n        !(node.classList && node.classList.contains(\"oe-nested\"))\n    ) {\n        delete processedStyle.display;\n    }\n    if (!processedStyle[\"box-sizing\"]) {\n        processedStyle[\"box-sizing\"] = \"border-box\"; // This is by default with Bootstrap.\n    }\n\n    // The css generates all the attributes separately and not in simplified\n    // form. In order to have a better compatibility (outlook for example) we\n    // simplify the css tags. e.g. border-left-style: none; border-bottom-s ....\n    // will be simplified in border-style = none\n    for (const info of [\n        { name: \"margin\" },\n        { name: \"padding\" },\n        { name: \"border\", suffix: \"-style\", defaultValue: \"none\" },\n    ]) {\n        const positions = [\"top\", \"right\", \"bottom\", \"left\"];\n        const positionalKeys = positions.map(\n            (position) => `${info.name}-${position}${info.suffix || \"\"}`\n        );\n        const styles = positionalKeys.map((key) => processedStyle[key]).filter((s) => s);\n        const hasVariableStyle = styles.some(\n            (style) => style.includes(\"calc(\") || style.includes(\"var(\")\n        );\n        const inherits = positionalKeys.some((key) =>\n            [\"inherit\", \"initial\"].includes((processedStyle[key] || \"\").trim())\n        );\n        if (styles.length && !hasVariableStyle && !inherits) {\n            const propertyName = `${info.name}${info.suffix || \"\"}`;\n            processedStyle[propertyName] = positionalKeys.every(\n                (key) => processedStyle[positionalKeys[0]] === processedStyle[key]\n            )\n                ? (processedStyle[propertyName] = processedStyle[positionalKeys[0]]) // top = right = bottom = left => property: [top];\n                : positionalKeys\n                      .map((key) => processedStyle[key] || info.defaultValue || 0)\n                      .join(\" \"); // property: [top] [right] [bottom] [left];\n            for (const prop of positionalKeys) {\n                delete processedStyle[prop];\n            }\n        }\n    }\n\n    if (processedStyle[\"border-bottom-left-radius\"]) {\n        processedStyle[\"border-radius\"] = processedStyle[\"border-bottom-left-radius\"];\n        delete processedStyle[\"border-bottom-left-radius\"];\n        delete processedStyle[\"border-bottom-right-radius\"];\n        delete processedStyle[\"border-top-left-radius\"];\n        delete processedStyle[\"border-top-right-radius\"];\n    }\n\n    // If the border styling is initial we remove it to simplify the css tags\n    // for compatibility. Also, since we do not send a css style tag, the\n    // initial value of the border is useless.\n    for (const styleName in processedStyle) {\n        if (styleName.includes(\"border\") && processedStyle[styleName] === \"initial\") {\n            delete processedStyle[styleName];\n        }\n    }\n\n    // text-decoration rule is decomposed in -line, -color and -style. This is\n    // however not supported by many browser/mail clients and the editor does\n    // not allow to change -color and -style rule anyway\n    if (processedStyle[\"text-decoration-line\"]) {\n        processedStyle[\"text-decoration\"] = processedStyle[\"text-decoration-line\"];\n        delete processedStyle[\"text-decoration-line\"];\n        delete processedStyle[\"text-decoration-color\"];\n        delete processedStyle[\"text-decoration-style\"];\n        delete processedStyle[\"text-decoration-thickness\"];\n    }\n\n    // flexboxes are not supported in Windows Outlook\n    for (const styleName in processedStyle) {\n        if (styleName.includes(\"flex\") || `${processedStyle[styleName]}`.includes(\"flex\")) {\n            delete processedStyle[styleName];\n        }\n    }\n\n    return processedStyle;\n}\nlet lastComputedStyleElement;\nlet lastComputedStyle;\n/**\n * Return the value of the given style property on the given element. This\n * caches the last computed style so if it's called several times in a row for\n * the same element, we don't recompute it every time.\n *\n * @param {Element} element\n * @param {string} propertyName\n * @returns\n */\nfunction _getStylePropertyValue(element, propertyName) {\n    const computedStyle =\n        lastComputedStyleElement === element ? lastComputedStyle : getComputedStyle(element);\n    lastComputedStyleElement = element;\n    lastComputedStyle = computedStyle;\n    return computedStyle[propertyName] || element.style.getPropertyValue(propertyName);\n}\n/**\n * Equivalent to JQuery's `width` method. Returns the element's visible width.\n *\n * @param {Element} element\n * @returns {Number}\n */\nfunction _getWidth(element) {\n    return parseFloat(getComputedStyle(element).width.replace(\"px\", \"\")) || 0;\n}\n/**\n * Equivalent to JQuery's `height` method. Returns the element's visible height.\n *\n * @param {Element} element\n * @returns {Number}\n */\nfunction _getHeight(element) {\n    return parseFloat(getComputedStyle(element).height.replace(\"px\", \"\")) || 0;\n}\n/**\n * Hides the given node (or just its opening/closing tag) for Outlook with mso\n * conditional comments and, if needed, mso hide style.\n *\n * @param {Node} node\n * @param {false|'opening'|'closing'} [onlyHideTag=false]\n */\nfunction _hideForOutlook(node, onlyHideTag = false) {\n    if (!onlyHideTag) {\n        node.setAttribute(\"style\", `${node.getAttribute(\"style\") || \"\"} mso-hide: all;`.trim());\n    }\n    node[onlyHideTag === \"closing\" ? \"append\" : \"before\"](document.createComment(\"[if !mso]><!\"));\n    node[onlyHideTag === \"opening\" ? \"prepend\" : \"after\"](document.createComment(\"<![endif]\"));\n}\n/**\n * Take a css style declaration return a \"normalized\" version of it (as a\n * standard object) for the purposes of emails. This means removing its styles\n * that are invalid, describe animations or aren't standard css (webkit\n * extensions). It also involves adding the \"!important\" suffix to styles that\n * have that priority, so they can be handled without access to the full\n * declaration.\n *\n * @param {CSSStyleDeclaration} style\n * @returns {Object} {[styleName]: string}\n */\nfunction _normalizeStyle(style) {\n    const normalizedStyle = {};\n    for (const styleName of style) {\n        const value = style[styleName];\n        if (\n            value &&\n            !styleName.includes(\"animation\") &&\n            !styleName.includes(\"-webkit\") &&\n            typeof value === \"string\"\n        ) {\n            const normalizedStyleName = styleName.replace(/-(.)/g, (a, b) => b.toUpperCase());\n            normalizedStyle[styleName] = style[normalizedStyleName];\n            if (style.getPropertyPriority(styleName) === \"important\") {\n                normalizedStyle[styleName] += \" !important\";\n            }\n        }\n    }\n    return normalizedStyle;\n}\n/**\n * Wrap a given element into a new parent, in place.\n *\n * @param {Element} element\n * @param {string} wrapperTag\n * @param {string} [wrapperClass] optional class to apply to the wrapper\n * @param {string} [wrapperStyle] optional style to apply to the wrapper\n * @returns {Element} the wrapper\n */\nfunction _wrap(element, wrapperTag, wrapperClass, wrapperStyle) {\n    const wrapper = document.createElement(wrapperTag);\n    if (wrapperClass) {\n        wrapper.className = wrapperClass;\n    }\n    if (wrapperStyle) {\n        wrapper.style.cssText = wrapperStyle;\n    }\n    element.parentElement.insertBefore(wrapper, element);\n    wrapper.append(element);\n    return wrapper;\n}\n", "import { HtmlField, htmlField } from \"@html_editor/fields/html_field\";\nimport { registry } from \"@web/core/registry\";\nimport { getCSSRules, toInline } from \"./convert_inline\";\nimport { ColumnPlugin } from \"@html_editor/main/column_plugin\";\n\nconst cssRulesByElement = new WeakMap();\n\nexport class HtmlMailField extends HtmlField {\n    /**\n     * @param {WeakMap} cssRulesByElement\n     * @param {Editor} editor\n     * @param {HTMLElement} el\n     */\n    static async getInlinedEditorContent(cssRulesByElement, editor, el) {\n        if (!cssRulesByElement.has(editor.editable)) {\n            cssRulesByElement.set(editor.editable, getCSSRules(editor.document));\n        }\n        const cssRules = cssRulesByElement.get(editor.editable);\n        // Insert the cloned element inside an DOM so we can get its computed style.\n        editor.editable.after(el);\n        el.classList.remove(\"odoo-editor-editable\");\n        await toInline(el, cssRules);\n        el.remove();\n    }\n\n    async getEditorContent() {\n        const el = await super.getEditorContent();\n        await HtmlMailField.getInlinedEditorContent(cssRulesByElement, this.editor, el);\n        return el;\n    }\n\n    getConfig() {\n        const config = super.getConfig();\n        config.dropImageAsAttachment = false;\n        config.Plugins = config.Plugins.filter((plugin) => plugin !== ColumnPlugin);\n        return config;\n    }\n}\n\nexport const htmlMailField = {\n    ...htmlField,\n    component: HtmlMailField,\n    additionalClasses: [\"o_field_html\"],\n    extractProps({ attrs, options }, dynamicInfo) {\n        const props = htmlField.extractProps({ attrs, options }, dynamicInfo);\n        props.embeddedComponents = false;\n        return props;\n    },\n};\n\nregistry.category(\"fields\").add(\"html_mail\", htmlMailField);\n", "import { ActivityButton } from \"@mail/core/web/activity_button\";\n\nimport { Component } from \"@odoo/owl\";\n\nimport { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nexport class KanbanActivity extends Component {\n    static components = { ActivityButton };\n    // used in children, in particular in ActivityButton\n    static fieldDependencies = [\n        {\n            name: \"activity_exception_decoration\",\n            type: \"selection\",\n            selection: [(\"warning\", \"Alert\"), (\"danger\", \"Error\")],\n        },\n        { name: \"activity_exception_icon\", type: \"char\" },\n        { name: \"activity_state\", type: \"selection\" },\n        { name: \"activity_summary\", type: \"char\" },\n        { name: \"activity_type_icon\", type: \"char\" },\n        { name: \"activity_type_id\", type: \"many2one\", relation: \"mail.activity.type\" },\n    ];\n    static props = standardFieldProps;\n    static template = \"mail.KanbanActivity\";\n}\n\nexport const kanbanActivity = {\n    component: KanbanActivity,\n    fieldDependencies: KanbanActivity.fieldDependencies,\n};\n\nregistry.category(\"fields\").add(\"kanban_activity\", kanbanActivity);\n", "import { ActivityButton } from \"@mail/core/web/activity_button\";\n\nimport { Component } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nexport class ListActivity extends Component {\n    static components = { ActivityButton };\n    // also used in children, in particular in ActivityButton\n    static fieldDependencies = [\n        { name: \"activity_exception_decoration\", type: \"selection\", selection: [] },\n        { name: \"activity_exception_icon\", type: \"char\" },\n        { name: \"activity_state\", type: \"selection\", selection: [] },\n        { name: \"activity_summary\", type: \"char\" },\n        { name: \"activity_type_icon\", type: \"char\" },\n        { name: \"activity_type_id\", type: \"many2one\", relation: \"mail.activity.type\" },\n    ];\n    static props = standardFieldProps;\n    static template = \"mail.ListActivity\";\n\n    get summaryText() {\n        if (this.props.record.data.activity_exception_decoration) {\n            return _t(\"Warning\");\n        }\n        if (this.props.record.data.activity_summary) {\n            return this.props.record.data.activity_summary;\n        }\n        if (this.props.record.data.activity_type_id) {\n            return this.props.record.data.activity_type_id[1 /* display_name */];\n        }\n        return undefined;\n    }\n}\n\nexport const listActivity = {\n    component: ListActivity,\n    fieldDependencies: ListActivity.fieldDependencies,\n    displayName: _t(\"List Activity\"),\n    supportedTypes: [\"one2many\"],\n};\n\nregistry.category(\"fields\").add(\"list_activity\", listActivity);\n", "import { useAssignUserCommand } from \"@mail/views/web/fields/assign_user_command_hook\";\n\nimport { registry } from \"@web/core/registry\";\nimport { TagsList } from \"@web/core/tags_list/tags_list\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { AvatarCardPopover } from \"@mail/discuss/web/avatar_card/avatar_card_popover\";\nimport {\n    Many2ManyTagsAvatarField,\n    many2ManyTagsAvatarField,\n    ListMany2ManyTagsAvatarField,\n    listMany2ManyTagsAvatarField,\n    KanbanMany2ManyTagsAvatarField,\n    kanbanMany2ManyTagsAvatarField,\n    KanbanMany2ManyTagsAvatarFieldTagsList,\n} from \"@web/views/fields/many2many_tags_avatar/many2many_tags_avatar_field\";\n\nexport class Many2ManyAvatarUserTagsList extends TagsList {\n    static template = \"mail.Many2ManyAvatarUserTagsList\";\n}\n\nconst WithUserChatter = (T) =>\n    class UserChatterMixin extends T {\n        setup() {\n            super.setup(...arguments);\n            if (this.props.withCommand) {\n                useAssignUserCommand();\n            }\n            this.avatarCard = usePopover(AvatarCardPopover);\n        }\n\n        displayAvatarCard(record) {\n            return this.relation === \"res.users\";\n        }\n\n        getAvatarCardProps(record) {\n            return {\n                id: record.resId,\n            };\n        }\n\n        getTagProps(record) {\n            return {\n                ...super.getTagProps(...arguments),\n                onImageClicked: (ev) => {\n                    if (!this.displayAvatarCard(record)) {\n                        return;\n                    }\n                    const target = ev.currentTarget;\n                    if (\n                        !this.avatarCard.isOpen ||\n                        (this.lastOpenedId && record.resId !== this.lastOpenedId)\n                    ) {\n                        this.avatarCard.open(target, this.getAvatarCardProps(record));\n                        this.lastOpenedId = record.resId;\n                    }\n                },\n            };\n        }\n    };\n\nexport class Many2ManyTagsAvatarUserField extends WithUserChatter(Many2ManyTagsAvatarField) {\n    static components = {\n        ...Many2ManyTagsAvatarField.components,\n        TagsList: Many2ManyAvatarUserTagsList,\n    };\n}\n\nexport const many2ManyTagsAvatarUserField = {\n    ...many2ManyTagsAvatarField,\n    component: Many2ManyTagsAvatarUserField,\n    additionalClasses: [\"o_field_many2many_tags_avatar\"],\n};\n\nregistry.category(\"fields\").add(\"many2many_avatar_user\", many2ManyTagsAvatarUserField);\n\nexport class KanbanMany2ManyAvatarUserTagsList extends KanbanMany2ManyTagsAvatarFieldTagsList {\n    static template = \"mail.KanbanMany2ManyAvatarUserTagsList\";\n}\n\nexport class KanbanMany2ManyTagsAvatarUserField extends WithUserChatter(\n    KanbanMany2ManyTagsAvatarField\n) {\n    static template = \"mail.KanbanMany2ManyTagsAvatarUserField\";\n    static components = {\n        ...KanbanMany2ManyTagsAvatarField.components,\n        TagsList: KanbanMany2ManyAvatarUserTagsList,\n    };\n    get displayText() {\n        return !this.props.readonly;\n    }\n}\nexport const kanbanMany2ManyTagsAvatarUserField = {\n    ...kanbanMany2ManyTagsAvatarField,\n    component: KanbanMany2ManyTagsAvatarUserField,\n    additionalClasses: [\"o_field_many2many_tags_avatar\", \"o_field_many2many_tags_avatar_kanban\"],\n};\nregistry.category(\"fields\").add(\"kanban.many2many_avatar_user\", kanbanMany2ManyTagsAvatarUserField);\n\nexport class ListMany2ManyTagsAvatarUserField extends WithUserChatter(\n    ListMany2ManyTagsAvatarField\n) {\n    static template = \"mail.ListMany2ManyTagsAvatarUserField\";\n    static components = {\n        ...ListMany2ManyTagsAvatarField.components,\n        TagsList: Many2ManyAvatarUserTagsList,\n    };\n\n    get displayText() {\n        return this.props.record.data[this.props.name].records.length === 1 || !this.props.readonly;\n    }\n}\n\nexport const listMany2ManyTagsAvatarUserField = {\n    ...listMany2ManyTagsAvatarField,\n    component: ListMany2ManyTagsAvatarUserField,\n    listViewWidth: [120],\n    additionalClasses: [\"o_field_many2many_tags_avatar\", \"o_field_many2many_tags_avatar_list\"],\n};\n\nregistry.category(\"fields\").add(\"list.many2many_avatar_user\", listMany2ManyTagsAvatarUserField);\nregistry\n    .category(\"fields\")\n    .add(\"activity.many2many_avatar_user\", kanbanMany2ManyTagsAvatarUserField);\n", "import { onMounted } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { TagsList } from \"@web/core/tags_list/tags_list\";\nimport {\n    Many2ManyTagsField,\n    many2ManyTagsField,\n} from \"@web/views/fields/many2many_tags/many2many_tags_field\";\nimport { useOpenMany2XRecord } from \"@web/views/fields/relational_utils\";\n\nexport class FieldMany2ManyTagsEmailTagsList extends TagsList {\n    static template = \"FieldMany2ManyTagsEmailTagsList\";\n}\n\nexport class FieldMany2ManyTagsEmail extends Many2ManyTagsField {\n    static components = {\n        ...FieldMany2ManyTagsEmail.components,\n        TagsList: FieldMany2ManyTagsEmailTagsList,\n    };\n    static props = {\n        ...Many2ManyTagsField.props,\n        context: { type: Object, optional: true },\n    };\n\n    setup() {\n        super.setup();\n\n        this.openedDialogs = 0;\n        this.recordsIdsToAdd = [];\n        this.openMany2xRecord = useOpenMany2XRecord({\n            resModel: this.relation,\n            activeActions: {\n                create: false,\n                createEdit: false,\n                write: true,\n            },\n            isToMany: true,\n            onRecordSaved: async (record) => {\n                if (record.data.email) {\n                    this.recordsIdsToAdd.push(record.resId);\n                }\n            },\n            fieldString: this.props.string,\n        });\n\n        const update = this.update;\n        this.update = async (object) => {\n            await update(object);\n            await this.checkEmails();\n        };\n\n        onMounted(() => {\n            this.checkEmails();\n        });\n    }\n\n    async checkEmails() {\n        const list = this.props.record.data[this.props.name];\n        const invalidRecords = list.records.filter((record) => !record.data.email);\n        if (!invalidRecords.length) {\n            return;\n        }\n        // Remove records with invalid data, open form view to edit those and readd them if they are updated correctly.\n        const dialogDefs = [];\n        for (const record of invalidRecords) {\n            dialogDefs.push(\n                this.openMany2xRecord({\n                    resId: record.resId,\n                    context: this.props.context,\n                    title: _t(\"Edit: %s\", record.data.display_name),\n                })\n            );\n        }\n        this.openedDialogs += invalidRecords.length;\n        await Promise.all(dialogDefs);\n\n        this.openedDialogs -= invalidRecords.length;\n        if (this.openedDialogs) {\n            return;\n        }\n\n        const invalidRecordIds = invalidRecords.map((rec) => rec.resId);\n        await list.addAndRemove({\n            remove: invalidRecordIds.filter((id) => !this.recordsIdsToAdd.includes(id)),\n            reload: true,\n        });\n        this.recordsIdsToAdd = [];\n    }\n\n    get tags() {\n        // Add email to our tags\n        const tags = super.tags;\n        const emailByResId = this.props.record.data[this.props.name].records.reduce(\n            (acc, record) => {\n                acc[record.resId] = record.data.email;\n                return acc;\n            },\n            {}\n        );\n        tags.forEach((tag) => (tag.email = emailByResId[tag.resId]));\n        return tags;\n    }\n}\n\nexport const fieldMany2ManyTagsEmail = {\n    ...many2ManyTagsField,\n    component: FieldMany2ManyTagsEmail,\n    extractProps(fieldInfo, dynamicInfo) {\n        const props = many2ManyTagsField.extractProps(...arguments);\n        props.context = dynamicInfo.context;\n        return props;\n    },\n    relatedFields: (fieldInfo) => {\n        return [...many2ManyTagsField.relatedFields(fieldInfo), { name: \"email\", type: \"char\" }];\n    },\n    additionalClasses: [\"o_field_many2many_tags\"],\n};\n\nregistry.category(\"fields\").add(\"many2many_tags_email\", fieldMany2ManyTagsEmail);\n", "import { useAssignUserCommand } from \"@mail/views/web/fields/assign_user_command_hook\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport {\n    Many2OneAvatarField,\n    many2OneAvatarField,\n    KanbanMany2OneAvatarField,\n    kanbanMany2OneAvatarField,\n} from \"@web/views/fields/many2one_avatar/many2one_avatar_field\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { AvatarCardPopover } from \"@mail/discuss/web/avatar_card/avatar_card_popover\";\n\nconst WithUserChatter = (T) =>\n    class extends T {\n        setup() {\n            super.setup(...arguments);\n            if (this.props.withCommand) {\n                useAssignUserCommand();\n            }\n            this.avatarCard = usePopover(AvatarCardPopover);\n        }\n\n        get displayAvatarCard() {\n            return this.relation === \"res.users\";\n        }\n\n        getAvatarCardProps() {\n            return {\n                id: this.props.record.data[this.props.name][0] ?? false,\n            };\n        }\n\n        onClickAvatar(ev) {\n            const id = this.props.record.data[this.props.name][0] ?? false;\n            if (id !== false) {\n                if (!this.displayAvatarCard) {\n                    return;\n                }\n                const target = ev.currentTarget;\n                if (!this.avatarCard.isOpen) {\n                    this.avatarCard.open(target, this.getAvatarCardProps());\n                }\n            }\n        }\n    };\n\nexport class Many2OneAvatarUserField extends WithUserChatter(Many2OneAvatarField) {\n    static template = \"mail.Many2OneAvatarUserField\";\n    static props = {\n        ...Many2OneAvatarField.props,\n        withCommand: { type: Boolean, optional: true },\n    };\n}\n\nexport const many2OneAvatarUserField = {\n    ...many2OneAvatarField,\n    component: Many2OneAvatarUserField,\n    additionalClasses: [\"o_field_many2one_avatar\"],\n    listViewWidth: [110],\n    extractProps(fieldInfo, dynamicInfo) {\n        const props = many2OneAvatarField.extractProps(...arguments);\n        props.withCommand = fieldInfo.viewType === \"form\" || fieldInfo.viewType === \"list\";\n        return props;\n    },\n};\n\nregistry.category(\"fields\").add(\"many2one_avatar_user\", many2OneAvatarUserField);\n\nexport class KanbanMany2OneAvatarUserField extends WithUserChatter(KanbanMany2OneAvatarField) {\n    static template = \"mail.KanbanMany2OneAvatarUserField\";\n    static props = {\n        ...KanbanMany2OneAvatarField.props,\n        displayAvatarName: { type: Boolean, optional: true },\n    };\n    /**\n     * All props are normally passed to the Many2OneField however since\n     * we add a new one, we need to filter it out.\n     */\n    get popoverProps() {\n        const props = super.popoverProps;\n        delete props.displayAvatarName;\n        return props;\n    }\n}\n\nexport const kanbanMany2OneAvatarUserField = {\n    ...kanbanMany2OneAvatarField,\n    component: KanbanMany2OneAvatarUserField,\n    additionalClasses: [...kanbanMany2OneAvatarField.additionalClasses, \"o_field_many2one_avatar\"],\n    supportedOptions: [\n        ...(kanbanMany2OneAvatarField.supportedOptions || []),\n        {\n            label: _t(\"Display avatar name\"),\n            name: \"display_avatar_name\",\n            type: \"boolean\",\n        },\n    ],\n    extractProps({ options }) {\n        const props = kanbanMany2OneAvatarField.extractProps(...arguments);\n        props.displayAvatarName = options.display_avatar_name || false;\n        return props;\n    },\n};\n\nregistry.category(\"fields\").add(\"kanban.many2one_avatar_user\", kanbanMany2OneAvatarUserField);\nregistry.category(\"fields\").add(\"activity.many2one_avatar_user\", kanbanMany2OneAvatarUserField);\n", "import { useOpenChat } from \"@mail/core/web/open_chat_hook\";\n\nimport { TagsList } from \"@web/core/tags_list/tags_list\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { PropertyValue } from \"@web/views/fields/properties/property_value\";\n\n/**\n * Allow to open the chatter of the user when we click on the avatar of a Many2one\n * property (like we do for many2one_avatar_user widget).\n */\npatch(PropertyValue.prototype, {\n    setup() {\n        super.setup();\n\n        if (this.env.services[\"mail.store\"]) {\n            // work only for the res.users model\n            this.openChat = useOpenChat(\"res.users\");\n        }\n    },\n\n    _onAvatarClicked() {\n        if (this.openChat && this.showAvatar && this.props.comodel === \"res.users\") {\n            this.openChat(this.props.value[0]);\n        }\n    },\n});\n\n/**\n * Allow to open the chatter of the user when we click on the avatar of a Many2many\n * property (like we do for many2many_avatar_user widget).\n */\nexport class Many2manyPropertiesTagsList extends TagsList {\n    static template = \"mail.Many2manyPropertiesTagsList\";\n\n    setup() {\n        super.setup();\n        if (this.env.services[\"mail.store\"]) {\n            this.openChat = useOpenChat(\"res.users\");\n        }\n    }\n\n    _onAvatarClicked(tagIndex) {\n        const tag = this.props.tags[tagIndex];\n        if (this.openChat && tag.comodel === \"res.users\") {\n            this.openChat(tag.id);\n        }\n    }\n}\n\nPropertyValue.components = {\n    ...PropertyValue.components,\n    TagsList: Many2manyPropertiesTagsList,\n};\n", "import { ListController } from \"@web/views/list/list_controller\";\n\nexport class ArchiveDisabledListController extends ListController {\n    setup() {\n        super.setup();\n        this.archiveEnabled = false;\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { listView } from \"@web/views/list/list_view\";\nimport { ArchiveDisabledListController } from \"./archive_disabled_list_controller\";\n\nexport const archiveDisabledListView = {\n    ...listView,\n    Controller: ArchiveDisabledListController,\n};\n\nregistry.category(\"views\").add(\"archive_disabled_activity_list\", archiveDisabledListView);\n", "import { patch } from \"@web/core/utils/patch\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\n\npatch(ListRenderer.prototype, {\n    getPropertyFieldColumns(_, list) {\n        const columns = super.getPropertyFieldColumns(...arguments);\n        for (const column of columns) {\n            const { relation, type } = list.fields[column.name];\n            if (relation === \"res.users\") {\n                column.widget =\n                    type === \"many2one\" ? \"many2one_avatar_user\" : \"many2many_avatar_user\";\n            }\n        }\n        return columns;\n    },\n});\n", "import { SampleServer } from \"@web/model/sample_server\";\nimport { patch } from \"@web/core/utils/patch\";\n\n/**\n * If `activity_exception_decoration` is set, 'Warning' is displayed\n * instead of the last activity, and we don't want to see a bunch of\n * 'Warning's in a list.\n */\npatch(SampleServer.prototype, {\n    _getRandomSelectionValue(modelName, field) {\n        if (field.name === \"activity_exception_decoration\") {\n            return false;\n        }\n        return super._getRandomSelectionValue(...arguments);\n    },\n});\n", "import { browser } from \"@web/core/browser/browser\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { WebClient } from \"@web/webclient/webclient\";\nimport { onWillDestroy } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nconst USER_DEVICES_MODEL = \"mail.push.device\";\n\npatch(WebClient.prototype, {\n    /**\n     * @override\n     */\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.notification = useService(\"notification\");\n        if (this._canSendNativeNotification) {\n            this._subscribePush();\n        }\n        if (browser.navigator.permissions) {\n            let notificationPerm;\n            const onPermissionChange = () => {\n                if (this._canSendNativeNotification) {\n                    this._subscribePush();\n                } else {\n                    this._unsubscribePush();\n                }\n            };\n            browser.navigator.permissions.query({ name: \"notifications\" }).then((perm) => {\n                notificationPerm = perm;\n                notificationPerm.addEventListener(\"change\", onPermissionChange);\n            });\n            onWillDestroy(() => {\n                notificationPerm?.removeEventListener(\"change\", onPermissionChange);\n            });\n        }\n    },\n    /**\n     *\n     * @returns {boolean}\n     * @private\n     */\n    get _canSendNativeNotification() {\n        return browser.Notification?.permission === \"granted\";\n    },\n\n    /**\n     * Subscribe device from push notification\n     *\n     * @private\n     * @return {Promise<void>}\n     */\n    async _subscribePush(numberTry = 1) {\n        const pushManager = await this.pushManager();\n        if (!pushManager) {\n            return;\n        }\n        let subscription = await pushManager.getSubscription();\n        const previousEndpoint = browser.localStorage.getItem(`${USER_DEVICES_MODEL}_endpoint`);\n        // This may occur if the subscription was refreshed by the browser,\n        // but it may also happen if the subscription has been revoked or lost.\n        if (!subscription) {\n            try {\n                subscription = await pushManager.subscribe({\n                    userVisibleOnly: true,\n                    applicationServerKey: await this._getApplicationServerKey(),\n                });\n            } catch (error) {\n                console.warn(error);\n                this.notification.add(error.message, {\n                    title: _t(\"Failed to enable push notifications\"),\n                    type: \"danger\",\n                    sticky: true,\n                });\n                if (await navigator.brave?.isBrave()) {\n                    this.notification.add(\n                        _t(\n                            \"Brave: enable 'Google Services for Push Messaging' to enable push notifications\"\n                        ),\n                        {\n                            type: \"warning\",\n                            sticky: true,\n                        }\n                    );\n                }\n                return;\n            }\n            browser.localStorage.setItem(`${USER_DEVICES_MODEL}_endpoint`, subscription.endpoint);\n        }\n        const kwargs = subscription.toJSON();\n        if (previousEndpoint && subscription.endpoint !== previousEndpoint) {\n            kwargs.previous_endpoint = previousEndpoint;\n        }\n        try {\n            kwargs.vapid_public_key = this._arrayBufferToBase64(\n                subscription.options.applicationServerKey\n            );\n            await this.orm.call(USER_DEVICES_MODEL, \"register_devices\", [], kwargs);\n        } catch (e) {\n            const invalidVapidErrorClass = \"odoo.addons.mail.tools.jwt.InvalidVapidError\";\n            const warningMessage = \"Error sending subscription information to the server\";\n            if (e.data?.name === invalidVapidErrorClass) {\n                const MAX_TRIES = 2;\n                if (numberTry < MAX_TRIES) {\n                    await subscription.unsubscribe();\n                    this._subscribePush(numberTry + 1);\n                } else {\n                    console.warn(warningMessage);\n                }\n            } else {\n                console.warn(`${warningMessage}: ${e.data?.debug}`);\n            }\n        }\n    },\n\n    /**\n     * Unsubscribe device from push notification\n     *\n     * @private\n     * @return {Promise<void>}\n     */\n    async _unsubscribePush() {\n        const pushManager = await this.pushManager();\n        if (!pushManager) {\n            return;\n        }\n        const subscription = await pushManager.getSubscription();\n        if (!subscription) {\n            return;\n        }\n        await this.orm.call(USER_DEVICES_MODEL, \"unregister_devices\", [], {\n            endpoint: subscription.endpoint,\n        });\n        await subscription.unsubscribe();\n        browser.localStorage.removeItem(`${USER_DEVICES_MODEL}_endpoint`);\n    },\n\n    /**\n     * Retrieve the PushManager interface of the Push API provides a way to receive notifications from third-party\n     * servers as well as request URLs for push notifications.\n     *\n     * @return {Promise<PushManager>}\n     */\n    async pushManager() {\n        const registration = await browser.navigator.serviceWorker?.getRegistration();\n        return registration?.pushManager;\n    },\n\n    /**\n     *\n     * The Application Server Key is need to be an Uint8Array.\n     * This format is used when the exchanging secret key between client and server.\n     * This base64 to Uint8Array implementation is inspired by https://github.com/gbhasha/base64-to-uint8array\n     *\n     * @private\n     * @return {Uint8Array}\n     */\n    async _getApplicationServerKey() {\n        const vapid_public_key_base64 = await this.orm.call(\n            USER_DEVICES_MODEL,\n            \"get_web_push_vapid_public_key\"\n        );\n        const padding = \"=\".repeat((4 - (vapid_public_key_base64.length % 4)) % 4);\n        const base64 = (vapid_public_key_base64 + padding).replace(/-/g, \"+\").replace(/_/g, \"/\");\n        const rawData = atob(base64);\n        const outputArray = new Uint8Array(rawData.length);\n        for (let i = 0; i < rawData.length; ++i) {\n            outputArray[i] = rawData.charCodeAt(i);\n        }\n        return outputArray;\n    },\n\n    /**\n     * Convert an ArrayBuffer to a base64 string without padding\n     * @param buffer {ArrayBuffer}\n     * @return {string}\n     * @private\n     */\n    _arrayBufferToBase64(buffer) {\n        const bytes = new Uint8Array(buffer);\n        let binary = \"\";\n        for (let i = 0; i < bytes.byteLength; i++) {\n            binary += String.fromCharCode(bytes[i]);\n        }\n        return window.btoa(binary).replaceAll(\"+\", \"-\").replaceAll(\"/\", \"_\").replaceAll(\"=\", \"\");\n    },\n});\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\";\nimport { useDebounced } from \"@web/core/utils/timing\";\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.rtc = useService(\"discuss.rtc\");\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        this.debouncedFetchPartnersToInvite = useDebounced(this.fetchPartnersToInvite.bind(this), 250);\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.debouncedFetchPartnersToInvite();\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                invite_to_rtc_call: this.rtc.state.channel?.eq(this.props.thread),\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            // Insert should always be done before any async operation. Indeed,\n            // awaiting before the insertion could lead to overwritting newer\n            // state coming from more recent `mail.record/insert` notifications.\n            this.store.insert(payload.data, { html: true });\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, silent, 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 = this.store.Message.get(data[\"mail.message\"][0]);\n        if (!message) {\n            return;\n        }\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            channel.selfMember\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, silent });\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 { createDocumentFragmentFromContent } from \"@mail/utils/common/html\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\nconst editAction = messageActionsRegistry.get(\"edit\");\n\npatch(editAction, {\n    onClick(component) {\n        const doc = createDocumentFragmentFromContent(component.message.body);\n        const mentionedChannelElements = doc.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    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        panelOuterClass: \"bg-100 border border-secondary\",\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 ${\n                component.props.chatWindow ? \"bg-inherit\" : \"\"\n            } bg-100 border border-secondary`;\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        close(component) {\n            if (component.env.inDiscussApp) {\n                component.store.discuss.isMemberPanelOpenByDefault = false;\n            }\n        },\n        open(component) {\n            if (component.env.inDiscussApp) {\n                component.store.discuss.isMemberPanelOpenByDefault = true;\n            }\n        },\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\n                    .filter((member) =>\n                        this.store.onlineMemberStatuses.includes(member.persona.im_status)\n                    )\n                    .sort((m1, m2) => this.store.sortMembers(m1, m2)); // FIXME: sort are prone to infinite loop (see test \"Display livechat custom name in typing status\")\n            },\n        });\n        this.offlineMembers = Record.many(\"ChannelMember\", {\n            compute() {\n                return this._computeOfflineMembers().sort(\n                    (m1, m2) => this.store.sortMembers(m1, m2) // FIXME: sort are prone to infinite loop (see test \"Display livechat custom name in typing status\")\n                );\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, { html: true });\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 { Component, useState } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {Object} Props\n * @extends {Component<Props, Env>}\n */\nexport class BusConnectionAlert extends Component {\n    static template = \"mail.BusConnectionAlert\";\n    static props = {};\n\n    setup() {\n        this.busMonitoring = useState(useService(\"bus.monitoring_service\"));\n        this.store = useState(useService(\"mail.store\"));\n    }\n}\n\nexport const connectionAlertService = {\n    dependencies: [\"bus.monitoring_service\", \"mail.store\"],\n    start() {\n        registry\n            .category(\"main_components\")\n            .add(\"bus.connection_alert\", { Component: BusConnectionAlert });\n    },\n};\nregistry.category(\"services\").add(\"bus.connection_alert\", connectionAlertService);\n", "import { DiscussClientAction } from \"@mail/core/public_web/discuss_client_action\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(DiscussClientAction.prototype, {\n    async restoreDiscussThread() {\n        await this.store.channels.fetch();\n        return super.restoreDiscussThread(...arguments);\n    },\n    parseActiveId(rawActiveId) {\n        if (typeof rawActiveId === \"number\") {\n            return [\"discuss.channel\", rawActiveId];\n        }\n        const [model, id] = super.parseActiveId(rawActiveId);\n        if (model === \"mail.channel\") {\n            // legacy format (sent in old emails, shared links, ...)\n            return [\"discuss.channel\", id];\n        }\n        return [model, id];\n    },\n});\n", "import { reactive } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport { registry } from \"@web/core/registry\";\n\nexport class DiscussCorePublicWeb {\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.store = services[\"mail.store\"];\n        this.busService = services.bus_service;\n        this.notificationService = services.notification;\n        try {\n            this.sidebarCategoriesBroadcast = new browser.BroadcastChannel(\n                \"discuss_core_public_web.sidebar_categories\"\n            );\n            this.sidebarCategoriesBroadcast.addEventListener(\n                \"message\",\n                ({ data: { id, open } }) => {\n                    const category = this.store.DiscussAppCategory.get(id);\n                    if (category) {\n                        category.open = open;\n                    }\n                }\n            );\n        } catch {\n            // BroadcastChannel API is not supported (e.g. Safari < 15.4), so disabling it.\n        }\n        this.busService.subscribe(\"discuss.channel/joined\", async (payload) => {\n            const { channel, invited_by_user_id: invitedByUserId } = payload;\n            const thread = this.store.Thread.insert(channel);\n            await thread.fetchChannelInfo();\n            if (invitedByUserId && invitedByUserId !== this.store.self.userId) {\n                this.notificationService.add(\n                    _t(\"You have been invited to #%s\", thread.displayName),\n                    { type: \"info\" }\n                );\n            }\n        });\n        browser.navigator.serviceWorker?.addEventListener(\n            \"message\",\n            async ({ data: { action, data } }) => {\n                if (action === \"OPEN_CHANNEL\") {\n                    const channel = await this.store.Thread.getOrFetch({\n                        model: \"discuss.channel\",\n                        id: data.id,\n                    });\n                    channel?.open();\n                }\n            }\n        );\n    }\n\n    /**\n     * Send the state of a category to the other tabs.\n     *\n     * @param {import(\"models\").DiscussAppCategory} category\n     */\n    broadcastCategoryState(category) {\n        this.sidebarCategoriesBroadcast?.postMessage({ id: category.id, open: category.open });\n    }\n}\n\nexport const discussCorePublicWeb = {\n    dependencies: [\"bus_service\", \"mail.store\", \"notification\"],\n\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {Partial<import(\"services\").Services>} services\n     */\n    start(env, services) {\n        return reactive(new DiscussCorePublicWeb(env, services));\n    },\n};\n\nregistry.category(\"services\").add(\"discuss.core.public.web\", discussCorePublicWeb);\n", "import { CountryFlag } from \"@mail/core/common/country_flag\";\nimport { ImStatus } from \"@mail/core/common/im_status\";\nimport { ThreadIcon } from \"@mail/core/common/thread_icon\";\nimport { discussSidebarItemsRegistry } from \"@mail/core/public_web/discuss_sidebar\";\nimport { cleanTerm } from \"@mail/utils/common/format\";\nimport { useHover } from \"@mail/utils/common/hooks\";\n\nimport { Component, useState, useSubEnv } from \"@odoo/owl\";\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 { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useAutofocus, useService } from \"@web/core/utils/hooks\";\nimport { markEventHandled } from \"@web/core/utils/misc\";\n\nexport const discussSidebarChannelIndicatorsRegistry = registry.category(\n    \"mail.discuss_sidebar_channel_indicators\"\n);\n\nexport class DiscussSidebarSubchannel extends Component {\n    static template = \"mail.DiscussSidebarSubchannel\";\n    static props = [\"thread\", \"isFirst?\"];\n    static components = { Dropdown };\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n        this.hover = useHover([\"root\", \"floating*\"], {\n            onHover: () => (this.floating.isOpen = true),\n            onAway: () => (this.floating.isOpen = false),\n        });\n        this.floating = useDropdownState();\n    }\n\n    get thread() {\n        return this.props.thread;\n    }\n\n    get commands() {\n        const commands = [];\n        if (this.thread.canUnpin) {\n            commands.push({\n                onSelect: () => this.thread.unpin(),\n                label: _t(\"Unpin Thread\"),\n                icon: \"oi oi-close\",\n                sequence: 20,\n            });\n        }\n        return commands;\n    }\n\n    get sortedCommands() {\n        const commands = [...this.commands];\n        commands.sort((c1, c2) => c1.sequence - c2.sequence);\n        return commands;\n    }\n\n    /** @param {MouseEvent} ev */\n    openThread(ev, thread) {\n        markEventHandled(ev, \"sidebar.openThread\");\n        thread.setAsDiscussThread();\n    }\n}\n\nexport class DiscussSidebarChannel extends Component {\n    static template = \"mail.DiscussSidebarChannel\";\n    static props = [\"thread\"];\n    static components = { CountryFlag, DiscussSidebarSubchannel, Dropdown, ImStatus, ThreadIcon };\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n        this.dialogService = useService(\"dialog\");\n        this.hover = useHover([\"root\", \"floating*\"], {\n            onHover: () => (this.floating.isOpen = true),\n            onAway: () => (this.floating.isOpen = false),\n        });\n        this.floating = useDropdownState();\n    }\n\n    get attClass() {\n        return {\n            \"bg-inherit\": this.thread.notEq(this.store.discuss.thread),\n            \"o-active\": this.thread.eq(this.store.discuss.thread),\n            \"o-unread\": this.thread.selfMember?.message_unread_counter > 0 && !this.thread.isMuted,\n            \"border-bottom-0 rounded-bottom-0\": this.bordered,\n            \"opacity-50\": this.thread.isMuted,\n            \"position-relative justify-content-center o-compact mt-0 p-1\":\n                this.store.discuss.isSidebarCompact,\n            \"px-0\": !this.store.discuss.isSidebarCompact,\n        };\n    }\n\n    get attClassContainer() {\n        return {\n            \"border border-dark rounded-2 o-bordered\": this.bordered,\n            \"o-compact\": this.store.discuss.isSidebarCompact,\n        };\n    }\n\n    get bordered() {\n        return (\n            this.store.discuss.isSidebarCompact &&\n            Boolean(this.env.filteredThreads?.(this.thread.sub_channel_ids)?.length)\n        );\n    }\n\n    get indicators() {\n        return discussSidebarChannelIndicatorsRegistry.getAll();\n    }\n\n    get commands() {\n        const commands = [];\n        if (this.thread.canLeave) {\n            commands.push({\n                onSelect: () => this.leaveChannel(),\n                label: _t(\"Leave Channel\"),\n                icon: \"oi oi-close\",\n                sequence: 20,\n            });\n        }\n        if (this.thread.canUnpin) {\n            commands.push({\n                onSelect: () => this.thread.unpin(),\n                label: _t(\"Unpin Conversation\"),\n                icon: \"oi oi-close\",\n                sequence: 20,\n            });\n        }\n        return commands;\n    }\n\n    get sortedCommands() {\n        const commands = [...this.commands];\n        commands.sort((c1, c2) => c1.sequence - c2.sequence);\n        return commands;\n    }\n\n    /** @returns {import(\"models\").Thread} */\n    get thread() {\n        return this.props.thread;\n    }\n\n    get subChannels() {\n        return this.env.filteredThreads?.(this.thread.sub_channel_ids) ?? [];\n    }\n\n    showThread(sub) {\n        if (sub.eq(this.store.discuss.thread)) {\n            return true;\n        }\n        if (!this.thread.discussAppCategory.open) {\n            return false;\n        }\n        if (!this.thread.isMuted || sub.selfMember?.message_unread_counter > 0) {\n            return true;\n        }\n        return this.isSelfOrThreadActive && !(this.thread.isMuted && sub.isMuted);\n    }\n\n    get isSelfOrThreadActive() {\n        return (\n            this.thread.eq(this.store.discuss.thread) ||\n            this.store.discuss.thread?.in(this.subChannels)\n        );\n    }\n\n    askConfirmation(body) {\n        return new Promise((resolve) => {\n            this.dialogService.add(ConfirmationDialog, {\n                body: body,\n                confirmLabel: _t(\"Leave Conversation\"),\n                confirm: resolve,\n                cancel: () => {},\n            });\n        });\n    }\n\n    async leaveChannel() {\n        const thread = this.thread;\n        if (thread.channel_type !== \"group\" && thread.create_uid === thread.store.self.userId) {\n            await this.askConfirmation(\n                _t(\"You are the administrator of this channel. Are you sure you want to leave?\")\n            );\n        }\n        if (thread.channel_type === \"group\") {\n            await this.askConfirmation(\n                _t(\n                    \"You are about to leave this group conversation and will no longer have access to it unless you are invited again. Are you sure you want to continue?\"\n                )\n            );\n        }\n        thread.leave();\n    }\n\n    /** @param {MouseEvent} ev */\n    openThread(ev, thread) {\n        markEventHandled(ev, \"sidebar.openThread\");\n        thread.setAsDiscussThread();\n    }\n}\n\nexport class DiscussSidebarCategory extends Component {\n    static template = \"mail.DiscussSidebarCategory\";\n    static props = [\"category\"];\n    static components = { Dropdown };\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n        this.discusscorePublicWebService = useState(useService(\"discuss.core.public.web\"));\n        this.hover = useHover([\"root\", \"floating*\"], {\n            onHover: () => this.onHover(true),\n            onAway: () => this.onHover(false),\n        });\n        this.floating = useDropdownState();\n    }\n\n    onHover(hovering) {\n        this.floating.isOpen = hovering;\n    }\n\n    /** @returns {import(\"models\").DiscussAppCategory} */\n    get category() {\n        return this.props.category;\n    }\n\n    get actions() {\n        return [];\n    }\n\n    toggle() {\n        if (this.store.channels.status === \"fetching\") {\n            return;\n        }\n        this.category.open = !this.category.open;\n        this.discusscorePublicWebService.broadcastCategoryState(this.category);\n    }\n}\n\nexport class DiscussSidebarQuickSearchInput extends Component {\n    static template = \"mail.DiscussSidebarQuickSearchInput\";\n    static props = [\"state\", \"autofocus?\"];\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n        if (this.props.autofocus) {\n            useAutofocus({ refName: \"root\" });\n        }\n    }\n\n    get state() {\n        return this.props.state;\n    }\n}\n\n/**\n * @typedef {Object} Props\n * @extends {Component<Props, Env>}\n */\nexport class DiscussSidebarCategories extends Component {\n    static template = \"mail.DiscussSidebarCategories\";\n    static props = {};\n    static components = {\n        DiscussSidebarCategory,\n        DiscussSidebarChannel,\n        DiscussSidebarQuickSearchInput,\n        Dropdown,\n    };\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n        this.discusscorePublicWebService = useState(useService(\"discuss.core.public.web\"));\n        this.state = useState({ quickSearchVal: \"\", floatingQuickSearchOpen: false });\n        this.orm = useService(\"orm\");\n        this.quickSearchHover = useHover([\"quick-search-btn\", \"quick-search-floating*\"], {\n            onHover: () => (this.quickSearchFloating.isOpen = true),\n            onAway: () => {\n                if (!this.quickSearchHover.isHover && !this.state.quickSearchVal.length) {\n                    this.state.floatingQuickSearchOpen = false;\n                }\n            },\n        });\n        this.quickSearchFloating = useDropdownState();\n        useSubEnv({\n            filteredThreads: (threads) => this.filteredThreads(threads),\n        });\n    }\n\n    filteredThreads(threads) {\n        return threads.filter(\n            (thread) =>\n                thread.displayInSidebar &&\n                (thread.parent_channel_id ||\n                    !this.state.quickSearchVal ||\n                    cleanTerm(thread.displayName).includes(cleanTerm(this.state.quickSearchVal)))\n        );\n    }\n\n    get hasQuickSearch() {\n        return (\n            Object.values(this.store.Thread.records).filter(\n                (thread) => thread.is_pinned && thread.model === \"discuss.channel\"\n            ).length > 19\n        );\n    }\n}\n\ndiscussSidebarItemsRegistry.add(\"channels\", DiscussSidebarCategories, { sequence: 30 });\n", "import { messageActionsRegistry } from \"@mail/core/common/message_actions\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nmessageActionsRegistry.add(\"create-or-view-thread\", {\n    condition: (component) =>\n        component.message.thread?.eq(component.props.thread) &&\n        component.message.thread.hasSubChannelFeature &&\n        component.store.self.isInternalUser,\n    icon: \"fa fa-comments-o\",\n    onClick: (component) => {\n        if (component.message.linkedSubChannel) {\n            component.message.linkedSubChannel.open();\n        } else {\n            component.message.thread.createSubChannel({ initialMessage: component.message });\n        }\n    },\n    title: (component) =>\n        component.message.linkedSubChannel ? _t(\"View Thread\") : _t(\"Create Thread\"),\n    sequence: 75,\n});\n", "import { Message } from \"@mail/core/common/message_model\";\nimport { Record } from \"@mail/model/record\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Message.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.linkedSubChannel = Record.one(\"Thread\", { inverse: \"from_message_id\" });\n    },\n});\n", "import { Message } from \"@mail/core/common/message\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Message.prototype, {\n    /**\n     * @override\n     * @param {MouseEvent} ev\n     */\n    async onClickNotificationMessage(ev) {\n        const { oeType } = ev.target.dataset;\n        if (oeType === \"sub-channels-menu\") {\n            this.env.subChannelMenu?.open();\n        }\n        await super.onClickNotificationMessage(...arguments);\n    },\n});\n", "import { Store } from \"@mail/core/common/store_service\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {import(\"models\").Store} */\nconst StorePatch = {\n    setup() {\n        super.setup(...arguments);\n        this.channels = this.makeCachedFetchData({ channels_as_member: true });\n    },\n};\npatch(Store.prototype, StorePatch);\n", "import { NotificationItem } from \"@mail/core/public_web/notification_item\";\nimport { ActionPanel } from \"@mail/discuss/core/common/action_panel\";\nimport { isToday } from \"@mail/utils/common/dates\";\nimport { useSequential, useVisible } from \"@mail/utils/common/hooks\";\nimport { Component, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useAutofocus, useService } from \"@web/core/utils/hooks\";\nimport { fuzzyLookup } from \"@web/core/utils/search\";\n\nconst { DateTime } = luxon;\n\n/**\n * @typedef {Object} Props\n * @property {import(\"@mail/core/common/thread_model\").Thread} thread\n * @property {function} [close]\n * @extends {Component<Props, Env>}\n */\nexport class SubChannelList extends Component {\n    static template = \"mail.SubChannelList\";\n    static components = { ActionPanel, NotificationItem };\n\n    static props = [\"thread\", \"close?\"];\n\n    setup() {\n        this.store = useService(\"mail.store\");\n        this.state = useState({\n            loading: false,\n            searchTerm: \"\",\n            lastSearchTerm: \"\",\n            searching: false,\n            subChannels: this.props.thread.sub_channel_ids,\n        });\n        this.searchRef = useRef(\"search\");\n        this.sequential = useSequential();\n        useAutofocus({ refName: \"search\" });\n        useVisible(\"load-more\", (isVisible) => {\n            if (isVisible) {\n                this.props.thread.loadMoreSubChannels({\n                    searchTerm: this.state.searching ? this.state.searchTerm : undefined,\n                });\n            }\n        });\n        useEffect(\n            (searchTerm) => {\n                if (!searchTerm) {\n                    this.clearSearch();\n                }\n            },\n            () => [this.state.searchTerm]\n        );\n    }\n\n    get NO_THREAD_FOUND() {\n        return _t(`No thread named \"%(thread_name)s\"`, { thread_name: this.state.lastSearchTerm });\n    }\n\n    async onClickSubThread(subThread) {\n        if (!subThread.hasSelfAsMember) {\n            await rpc(\"/discuss/channel/join\", { channel_id: subThread.id });\n        }\n        subThread.open();\n        if (this.env.inChatWindow) {\n            this.props.close?.();\n        }\n    }\n\n    clearSearch() {\n        this.state.searchTerm = \"\";\n        this.state.lastSearchTerm = \"\";\n        this.state.searching = false;\n        this.state.loading = false;\n        this.state.subChannels = this.props.thread.sub_channel_ids;\n    }\n\n    dateText(message) {\n        if (isToday(message.datetime)) {\n            return message.datetime?.toLocaleString(DateTime.TIME_SIMPLE);\n        }\n        return message.datetime?.toLocaleString(DateTime.DATE_MED);\n    }\n\n    onKeydownSearch(ev) {\n        if (ev.key === \"Enter\") {\n            this.search();\n        }\n    }\n\n    async onClickCreate() {\n        await this.props.thread.createSubChannel({ name: this.state.searchTerm });\n        this._refreshSubChannelList();\n        this.props.close?.();\n    }\n\n    async search() {\n        if (!this.state.searchTerm) {\n            return;\n        }\n        this.sequential(async () => {\n            this.state.searching = true;\n            this.state.loading = true;\n            try {\n                await this.props.thread.loadMoreSubChannels({\n                    searchTerm: this.state.searchTerm,\n                });\n                if (this.state.searching) {\n                    this._refreshSubChannelList();\n                    this.state.lastSearchTerm = this.state.searchTerm;\n                }\n            } finally {\n                this.state.loading = false;\n            }\n        });\n    }\n\n    _refreshSubChannelList() {\n        this.state.subChannels = fuzzyLookup(\n            this.state.searchTerm ?? \"\",\n            this.props.thread.sub_channel_ids,\n            ({ name }) => name\n        );\n    }\n}\n", "import { threadActionsRegistry } from \"@mail/core/common/thread_actions\";\nimport { SubChannelList } from \"@mail/discuss/core/public_web/sub_channel_list\";\nimport { useChildSubEnv, useComponent } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\n\nthreadActionsRegistry.add(\"show-threads\", {\n    close(component, action) {\n        action.popover?.close();\n    },\n    condition: (component) =>\n        component.thread?.hasSubChannelFeature ||\n        component.thread?.parent_channel_id?.hasSubChannelFeature,\n    icon: \"fa fa-fw fa-comments-o\",\n    iconLarge: \"fa fa-fw fa-lg fa-comments-o\",\n    name: _t(\"Threads\"),\n    component: SubChannelList,\n    componentProps(action) {\n        return { close: () => action.close() };\n    },\n    setup(action) {\n        const component = useComponent();\n        if (!component.props.chatWindow) {\n            action.popover = usePopover(SubChannelList, {\n                onClose: () => action.close(),\n                fixedPosition: true,\n                popoverClass: action.panelOuterClass,\n            });\n        }\n        useChildSubEnv({\n            subChannelMenu: {\n                open: () => action.open(),\n            },\n        });\n    },\n    open: (component, action) => {\n        const thread = component.thread?.parent_channel_id || component.thread;\n        action.popover?.open(component.root.el.querySelector(`[name=\"${action.id}\"]`), { thread });\n    },\n    panelOuterClass: \"bg-100 border border-secondary\",\n    sequence: (comp) => (comp.props.chatWindow ? 40 : 5),\n    sequenceGroup: 10,\n    toggle: true,\n});\n", "import { Thread } from \"@mail/core/common/thread_model\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { Record } from \"@mail/model/record\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Thread, {\n    async getOrFetch(data) {\n        let thread = super.get(data);\n        if (data.model !== \"discuss.channel\" || !data.id) {\n            return thread;\n        }\n        thread = this.insert({ id: data.id, model: data.model });\n        if (thread.fetchChannelInfoState === \"fetched\") {\n            return Promise.resolve(thread);\n        }\n        if (thread.fetchChannelInfoState === \"fetching\") {\n            return thread.fetchChannelInfoDeferred;\n        }\n        thread.fetchChannelInfoState = \"fetching\";\n        const def = new Deferred();\n        thread.fetchChannelInfoDeferred = def;\n        thread.fetchChannelInfo().then(\n            (result) => {\n                if (thread.exists()) {\n                    thread.fetchChannelInfoState = \"fetched\";\n                    thread.fetchChannelInfoDeferred = undefined;\n                }\n                def.resolve(result);\n            },\n            (error) => {\n                if (thread.exists()) {\n                    thread.fetchChannelInfoState = \"not_fetched\";\n                    thread.fetchChannelInfoDeferred = undefined;\n                }\n                def.reject(error);\n            }\n        );\n        return def;\n    },\n});\n\npatch(Thread.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.from_message_id = Record.one(\"Message\");\n        this.parent_channel_id = Record.one(\"Thread\", {\n            onDelete() {\n                this.delete();\n            },\n        });\n        this.sub_channel_ids = Record.many(\"Thread\", {\n            inverse: \"parent_channel_id\",\n            sort: (a, b) => b.id - a.id,\n        });\n        this.displayInSidebar = Record.attr(false, {\n            compute() {\n                return (\n                    this.displayToSelf ||\n                    this.isLocallyPinned ||\n                    this.sub_channel_ids.some((t) => t.displayInSidebar)\n                );\n            },\n        });\n        this.loadSubChannelsDone = false;\n        this.lastSubChannelLoaded = null;\n    },\n    get canLeave() {\n        return !this.parent_channel_id && super.canLeave;\n    },\n    get canUnpin() {\n        return (this.parent_channel_id && this.importantCounter === 0) || super.canUnpin;\n    },\n    get allowCalls() {\n        return super.allowCalls && !this.parent_channel_id;\n    },\n    delete() {\n        if (this.model === \"discuss.channel\") {\n            this.store.env.services.bus_service.deleteChannel(this.busChannel);\n        }\n        super.delete(...arguments);\n    },\n    get hasSubChannelFeature() {\n        return this.channel_type === \"channel\" && !this.parent_channel_id;\n    },\n    get isEmpty() {\n        return !this.from_message_id && super.isEmpty;\n    },\n    get notifyOnLeave() {\n        return super.notifyOnLeave && !this.parent_channel_id;\n    },\n    /**\n     * @param {Object} [param0={}]\n     * @param {import(\"models\").Message} [param0.initialMessage]\n     * @param {string} [param0.name]\n     */\n    async createSubChannel({ initialMessage, name } = {}) {\n        const { data, sub_channel } = await rpc(\"/discuss/channel/sub_channel/create\", {\n            parent_channel_id: this.id,\n            from_message_id: initialMessage?.id,\n            name,\n        });\n        this.store.insert(data, { html: true });\n        this.store.Thread.get(sub_channel).open();\n    },\n    /**\n     * @param {*} param0\n     * @param {string} [param0.searchTerm]\n     * @returns {import(\"models\").Thread[]}\n     */\n    async loadMoreSubChannels({ searchTerm } = {}) {\n        if (this.loadSubChannelsDone) {\n            return;\n        }\n        const limit = 30;\n        const data = await rpc(\"/discuss/channel/sub_channel/fetch\", {\n            before: this.lastSubChannelLoaded?.id,\n            limit,\n            parent_channel_id: this.id,\n            search_term: searchTerm,\n        });\n        const { Thread: threads = [] } = this.store.insert(data, { html: true });\n        if (searchTerm) {\n            // Ignore holes in the sub-channel list that may arise when\n            // searching for a specific term.\n            return;\n        }\n        const subChannels = threads.filter((thread) => this.eq(thread.parent_channel_id));\n        this.lastSubChannelLoaded = subChannels.reduce(\n            (min, channel) => (!min || channel.id < min.id ? channel : min),\n            this.lastSubChannelLoaded\n        );\n        if (subChannels.length < limit) {\n            this.loadSubChannelsDone = true;\n        }\n        return subChannels;\n    },\n    onPinStateUpdated() {\n        super.onPinStateUpdated();\n        if (this.is_pinned) {\n            this.isLocallyPinned = false;\n        }\n        if (this.isLocallyPinned) {\n            this.store.env.services[\"bus_service\"].addChannel(this.busChannel);\n        } else {\n            this.store.env.services[\"bus_service\"].deleteChannel(this.busChannel);\n        }\n        if (!this.is_pinned && !this.isLocallyPinned) {\n            this.sub_channel_ids.forEach((c) => (c.isLocallyPinned = false));\n        }\n    },\n    setAsDiscussThread() {\n        super.setAsDiscussThread(...arguments);\n        if (!this.displayToSelf && this.model === \"discuss.channel\") {\n            this.isLocallyPinned = true;\n        }\n    },\n});\n", "import { Thread } from \"@mail/core/common/thread\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Thread.prototype, {\n    get orderedMessages() {\n        const result = super.orderedMessages;\n        if (this.props.thread.from_message_id && !this.props.thread.from_message_id.isEmpty) {\n            if (this.props.order === \"asc\") {\n                result.unshift(this.props.thread.from_message_id);\n            } else {\n                result.push(this.props.thread.from_message_id);\n            }\n        }\n        return result;\n    },\n});\n", "import { ChannelMemberList } from \"@mail/discuss/core/common/channel_member_list\";\nimport { AvatarCardPopover } from \"@mail/discuss/web/avatar_card/avatar_card_popover\";\n\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(ChannelMemberList.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.avatarCard = usePopover(AvatarCardPopover, {\n            position: \"right\",\n        });\n    },\n    onClickAvatar(ev, member) {\n        if (!this.canOpenChatWith(member)) {\n            return;\n        }\n        if (!this.avatarCard.isOpen) {\n            this.avatarCard.open(ev.currentTarget, {\n                id: member.persona.userId,\n            });\n        }\n    },\n});\nObject.assign(ChannelMemberList.components, { AvatarCardPopover });\n", "import { NavigableList } from \"@mail/core/common/navigable_list\";\nimport { cleanTerm } from \"@mail/utils/common/format\";\n\nimport { Component, useEffect, useRef, useState } from \"@odoo/owl\";\n\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { TagsList } from \"@web/core/tags_list/tags_list\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { isEventHandled, markEventHandled } from \"@web/core/utils/misc\";\nimport { useSequential } from \"@mail/utils/common/hooks\";\n\nexport class ChannelSelector extends Component {\n    static components = { TagsList, NavigableList };\n    static props = [\"category\", \"onValidate?\", \"autofocus?\", \"multiple?\", \"close?\"];\n    static defaultProps = { multiple: true };\n    static template = \"discuss.ChannelSelector\";\n\n    setup() {\n        super.setup();\n        this.discussCoreCommonService = useState(useService(\"discuss.core.common\"));\n        this.store = useState(useService(\"mail.store\"));\n        this.suggestionService = useService(\"mail.suggestion\");\n        this.orm = useService(\"orm\");\n        this.sequential = useSequential();\n        this.state = useState({\n            value: \"\",\n            selectedPartners: [],\n            navigableListProps: {\n                anchorRef: undefined,\n                position: \"bottom-fit\",\n                onSelect: (ev, option) => this.onSelect(option),\n                optionTemplate:\n                    this.props.category.id === \"channels\"\n                        ? \"discuss.ChannelSelector.channel\"\n                        : \"discuss.ChannelSelector.chat\",\n                options: [],\n                isLoading: false,\n            },\n        });\n        this.inputRef = useRef(\"input\");\n        this.rootRef = useRef(\"root\");\n        this.markEventHandled = markEventHandled;\n        useEffect(\n            () => {\n                this.state.navigableListProps.anchorRef = this.rootRef?.el;\n                this.state.navigableListProps.optionTemplate =\n                    this.props.category.id === \"channels\"\n                        ? \"discuss.ChannelSelector.channel\"\n                        : \"discuss.ChannelSelector.chat\";\n            },\n            () => [this.rootRef, this.props.category]\n        );\n        useEffect(\n            () => {\n                this.fetchSuggestions();\n            },\n            () => [this.state.value]\n        );\n        useEffect(\n            (focus) => {\n                if (focus && this.inputRef.el) {\n                    this.inputRef.el.focus();\n                }\n            },\n            () => [this.props.autofocus]\n        );\n    }\n\n    async fetchSuggestions() {\n        const cleanedTerm = cleanTerm(this.state.value);\n        if (cleanedTerm) {\n            if (this.props.category.id === \"channels\") {\n                const domain = [\n                    [\"parent_channel_id\", \"=\", false],\n                    [\"channel_type\", \"=\", \"channel\"],\n                    [\"name\", \"ilike\", cleanedTerm],\n                ];\n                const fields = [\"name\"];\n                const results = await this.sequential(async () => {\n                    this.state.navigableListProps.isLoading = true;\n                    const res = await this.orm.searchRead(\"discuss.channel\", domain, fields, {\n                        limit: 10,\n                    });\n                    this.state.navigableListProps.isLoading = false;\n                    return res;\n                });\n                if (!results) {\n                    this.state.navigableListProps.options = [];\n                    return;\n                }\n                const choices = results.map((channel) => {\n                    return {\n                        channelId: channel.id,\n                        classList: \"o-discuss-ChannelSelector-suggestion\",\n                        label: channel.name,\n                    };\n                });\n                choices.push({\n                    channelId: \"__create__\",\n                    classList: \"o-discuss-ChannelSelector-suggestion\",\n                    label: this.state.value,\n                });\n                this.state.navigableListProps.options = choices;\n                return;\n            }\n            if (this.props.category.id === \"chats\") {\n                const data = await this.sequential(async () => {\n                    this.state.navigableListProps.isLoading = true;\n                    const data = await this.orm.call(\"res.partner\", \"im_search\", [\n                        cleanedTerm,\n                        10,\n                        this.state.selectedPartners,\n                    ]);\n                    this.state.navigableListProps.isLoading = false;\n                    return data;\n                });\n                if (!data) {\n                    this.state.navigableListProps.options = [];\n                    return;\n                }\n                const { Persona: partners = [] } = this.store.insert(data);\n                const suggestions = this.suggestionService\n                    .sortPartnerSuggestions(partners, cleanedTerm)\n                    .map((suggestion) => {\n                        return {\n                            classList: \"o-discuss-ChannelSelector-suggestion\",\n                            label: suggestion.name,\n                            partner: suggestion,\n                        };\n                    });\n                if (this.store.self.name.includes(cleanedTerm)) {\n                    suggestions.push({\n                        classList: \"o-discuss-ChannelSelector-suggestion\",\n                        label: this.store.self.name,\n                        partner: this.store.self,\n                    });\n                }\n                if (suggestions.length === 0) {\n                    suggestions.push({\n                        classList: \"o-discuss-ChannelSelector-suggestion\",\n                        label: _t(\"No results found\"),\n                        unselectable: true,\n                    });\n                }\n                this.state.navigableListProps.options = suggestions;\n                return;\n            }\n        }\n        this.state.navigableListProps.options = [];\n        return;\n    }\n\n    onSelect(option) {\n        if (this.props.category.id === \"channels\") {\n            if (option.channelId === \"__create__\") {\n                this.env.services.orm\n                    .call(\"discuss.channel\", \"channel_create\", [\n                        option.label,\n                        this.store.internalUserGroupId,\n                    ])\n                    .then((data) => {\n                        const { Thread } = this.store.insert(data);\n                        const [channel] = Thread;\n                        channel.open();\n                    });\n            } else {\n                this.store.joinChannel(option.channelId, option.label);\n            }\n            this.onValidate();\n        }\n        if (this.props.category.id === \"chats\") {\n            if (!this.state.selectedPartners.includes(option.partner.id)) {\n                this.state.selectedPartners.push(option.partner.id);\n            }\n            this.state.value = \"\";\n        }\n        if (!this.props.multiple) {\n            this.onValidate();\n        }\n    }\n\n    async onValidate() {\n        if (this.props.category.id === \"chats\") {\n            const selectedPartnerIds = this.state.selectedPartners;\n            if (selectedPartnerIds.length === 0) {\n                return;\n            }\n            await this.discussCoreCommonService.startChat(selectedPartnerIds);\n        }\n        if (this.props.onValidate) {\n            this.props.onValidate();\n        }\n    }\n\n    onKeydownInput(ev) {\n        const hotkey = getActiveHotkey(ev);\n        switch (hotkey) {\n            case \"enter\":\n                if (isEventHandled(ev, \"NavigableList.select\") || !this.state.value === \"\") {\n                    return;\n                }\n                this.onValidate();\n                break;\n            case \"backspace\":\n                if (this.state.selectedPartners.length > 0 && this.state.value === \"\") {\n                    this.state.selectedPartners.pop();\n                }\n                return;\n            case \"escape\":\n                this.props.close?.();\n                return;\n            default:\n                return;\n        }\n        ev.stopPropagation();\n        ev.preventDefault();\n    }\n\n    removeFromSelectedPartners(id) {\n        this.state.selectedPartners = this.state.selectedPartners.filter(\n            (partnerId) => partnerId !== id\n        );\n        this.inputRef.el.focus();\n    }\n\n    get inputPlaceholder() {\n        return this.state.selectedPartners.length > 0\n            ? _t(\"Press Enter to start\")\n            : this.props.category.addTitle;\n    }\n\n    get tagsList() {\n        const res = [];\n        for (const partnerId of this.state.selectedPartners) {\n            const partner = this.store.Persona.get({ type: \"partner\", id: partnerId });\n            res.push({\n                id: partner.id,\n                text: partner.name,\n                className: \"m-1 py-1\",\n                colorIndex: Math.floor(partner.name.length % 10),\n                onDelete: () => this.removeFromSelectedPartners(partnerId),\n            });\n        }\n        return res;\n    }\n}\n", "import { ChannelSelector } from \"@mail/discuss/core/web/channel_selector\";\nimport { ChatWindow } from \"@mail/core/common/chat_window\";\n\nObject.assign(ChatWindow.components, { ChannelSelector });\n", "import { cleanTerm } from \"@mail/utils/common/format\";\n\nimport { Component } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { imageUrl } from \"@web/core/utils/urls\";\nimport { ImStatus } from \"@mail/core/common/im_status\";\n\nconst commandSetupRegistry = registry.category(\"command_setup\");\nconst commandProviderRegistry = registry.category(\"command_provider\");\n\nclass DiscussCommand extends Component {\n    static components = { ImStatus };\n    static template = \"mail.DiscussCommand\";\n    static props = {\n        counter: { type: Number, optional: true },\n        executeCommand: Function,\n        imgUrl: String,\n        name: String,\n        persona: { type: Object, optional: true },\n        channel: { type: Object, optional: true },\n        searchValue: String,\n        slots: Object,\n    };\n}\n\n// -----------------------------------------------------------------------------\n// add @ namespace + provider\n// -----------------------------------------------------------------------------\ncommandSetupRegistry.add(\"@\", {\n    debounceDelay: 200,\n    emptyMessage: _t(\"No user found\"),\n    name: _t(\"users\"),\n    placeholder: _t(\"Search for a user...\"),\n});\n\ncommandProviderRegistry.add(\"mail.partner\", {\n    namespace: \"@\",\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     */\n    async provide(env, options) {\n        const store = env.services[\"mail.store\"];\n        await store.channels.fetch();\n        const suggestionService = env.services[\"mail.suggestion\"];\n        const commands = [];\n        const mentionedChannels = store.getNeedactionChannels();\n        // We don't want to display the same channel twice in the command palette.\n        const displayedPartnerIds = new Set();\n        if (!options.searchValue) {\n            mentionedChannels.slice(0, 3).map((channel) => {\n                if (channel.channel_type === \"chat\") {\n                    displayedPartnerIds.add(channel.correspondent.persona.id);\n                }\n                commands.push({\n                    Component: DiscussCommand,\n                    async action() {\n                        switch (channel.channel_type) {\n                            case \"chat\":\n                                store.openChat({ partnerId: channel.correspondent.persona.id });\n                                break;\n                            case \"group\":\n                                channel.open();\n                                break;\n                            case \"channel\": {\n                                await store.joinChannel(channel.id, channel.name);\n                                channel.open();\n                            }\n                        }\n                    },\n                    name: channel.displayName,\n                    category: \"discuss_mentioned\",\n                    props: {\n                        imgUrl: channel.avatarUrl,\n                        persona:\n                            channel.channel_type === \"chat\"\n                                ? channel.correspondent.persona\n                                : undefined,\n                        counter: channel.importantCounter,\n                    },\n                });\n            });\n        }\n        const searchResults = await store.searchPartners(options.searchValue);\n        suggestionService\n            .sortPartnerSuggestions(searchResults, options.searchValue)\n            .filter((partner) => !displayedPartnerIds.has(partner.id))\n            .map((partner) => {\n                const chat = partner.searchChat();\n                commands.push({\n                    Component: DiscussCommand,\n                    action() {\n                        store.openChat({ partnerId: partner.id });\n                    },\n                    name: partner.name,\n                    props: {\n                        imgUrl: partner.avatarUrl,\n                        persona: partner,\n                        counter: chat ? chat.importantCounter : undefined,\n                    },\n                });\n            });\n        return commands;\n    },\n});\n\n// -----------------------------------------------------------------------------\n// add # namespace + provider\n// -----------------------------------------------------------------------------\n\ncommandSetupRegistry.add(\"#\", {\n    debounceDelay: 200,\n    emptyMessage: _t(\"No channel found\"),\n    name: _t(\"channels\"),\n    placeholder: _t(\"Search for a channel...\"),\n});\n\ncommandProviderRegistry.add(\"discuss.channel\", {\n    namespace: \"#\",\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     */\n    async provide(env, options) {\n        const store = env.services[\"mail.store\"];\n        await store.channels.fetch();\n        const commands = [];\n        const recentChannels = store.getRecentChannels();\n        // We don't want to display the same thread twice in the command palette.\n        const shownChannels = new Set();\n        if (!options.searchValue) {\n            recentChannels\n                .filter((channel) => [\"channel\", \"group\"].includes(channel.channel_type))\n                .slice(0, 3)\n                .map((channel) => {\n                    shownChannels.add(channel.id);\n                    commands.push({\n                        Component: DiscussCommand,\n                        async action() {\n                            await store.joinChannel(channel.id, channel.name);\n                            channel.open();\n                        },\n                        name: channel.displayName,\n                        category: \"discuss_recent\",\n                        props: {\n                            channel,\n                            imgUrl: channel.avatarUrl,\n                            counter: channel.importantCounter,\n                        },\n                    });\n                });\n        }\n        const domain = [\n            [\"channel_type\", \"=\", \"channel\"],\n            [\"name\", \"ilike\", cleanTerm(options.searchValue)],\n        ];\n        const channelsData = await env.services.orm.searchRead(\n            \"discuss.channel\",\n            domain,\n            [\"channel_type\", \"name\", \"avatar_cache_key\", \"parent_channel_id\"],\n            { limit: 10 }\n        );\n        channelsData\n            .filter((data) => !shownChannels.has(data.id))\n            .map((data) => {\n                const channel = store.Thread.insert({\n                    ...data,\n                    model: \"discuss.channel\",\n                    parent_channel_id: data.parent_channel_id\n                        ? {\n                              id: data.parent_channel_id[0],\n                              model: \"discuss.channel\",\n                              name: data.parent_channel_id[1],\n                          }\n                        : null,\n                });\n                commands.push({\n                    Component: DiscussCommand,\n                    async action() {\n                        const channel = await store.joinChannel(data.id, data.name);\n                        channel.open();\n                    },\n                    name: data.name,\n                    props: {\n                        channel,\n                        imgUrl: imageUrl(\"discuss.channel\", data.id, \"avatar_128\", {\n                            unique: data.avatar_cache_key,\n                        }),\n                    },\n                });\n            });\n        const groups = recentChannels.filter(\n            (channel) =>\n                !shownChannels.has(channel.id) &&\n                channel.channel_type === \"group\" &&\n                cleanTerm(channel.displayName).includes(cleanTerm(options.searchValue))\n        );\n        groups.map((channel) => {\n            commands.push({\n                Component: DiscussCommand,\n                async action() {\n                    channel.open();\n                },\n                name: channel.displayName,\n                props: {\n                    imgUrl: channel.avatarUrl,\n                    counter: channel.importantCounter,\n                },\n            });\n        });\n        return commands;\n    },\n});\n", "import { DiscussCoreCommon } from \"@mail/discuss/core/common/discuss_core_common_service\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {DiscussCoreCommon} */\nconst discussCoreCommon = {\n    async _handleNotificationNewMessage(...args) {\n        // initChannelsUnreadCounter becomes unreliable\n        await this.store.channels.fetch();\n        return super._handleNotificationNewMessage(...args);\n    },\n};\n\npatch(DiscussCoreCommon.prototype, discussCoreCommon);\n", "import { reactive } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nexport class DiscussCoreWeb {\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.notificationService = services.notification;\n        this.ui = services.ui;\n        this.store = services[\"mail.store\"];\n        this.multiTab = services.multi_tab;\n    }\n\n    setup() {\n        this.busService.subscribe(\"res.users/connection\", async ({ partnerId, username }) => {\n            // If the current user invited a new user, and the new user is\n            // connecting for the first time while the current user is present\n            // then open a chat for the current user with the new user.\n            const notification = _t(\n                \"%(user)s connected. This is their first connection. Wish them luck.\",\n                { user: username }\n            );\n            this.notificationService.add(notification, { type: \"info\" });\n            if (!this.multiTab.isOnMainTab()) {\n                return;\n            }\n            const chat = await this.store.getChat({ partnerId });\n            if (chat && !this.ui.isSmall) {\n                this.store.chatHub.opened.add({ thread: chat });\n            }\n        });\n        this.busService.subscribe(\"discuss.Thread/fold_state\", async (data) => {\n            const thread = await this.store.Thread.getOrFetch(data);\n            if (data.fold_state && thread && data.foldStateCount > thread.foldStateCount) {\n                thread.foldStateCount = data.foldStateCount;\n                thread.state = data.fold_state;\n                if (thread.state === \"closed\") {\n                    const chatWindow = this.store.ChatWindow.get({ thread });\n                    chatWindow?.close({ notifyState: false });\n                }\n            }\n        });\n        this.env.bus.addEventListener(\"mail.message/delete\", ({ detail: { message } }) => {\n            if (message.thread?.model === \"discuss.channel\") {\n                // initChannelsUnreadCounter becomes unreliable\n                this.store.channels.fetch();\n            }\n        });\n        this.busService.start();\n    }\n}\n\nexport const discussCoreWeb = {\n    dependencies: [\"bus_service\", \"mail.store\", \"notification\", \"ui\", \"multi_tab\"],\n    /**\n     * @param {import(\"@web/env\").OdooEnv} env\n     * @param {Partial<import(\"services\").Services>} services\n     */\n    start(env, services) {\n        const discussCoreWeb = reactive(new DiscussCoreWeb(env, services));\n        discussCoreWeb.setup();\n        return discussCoreWeb;\n    },\n};\n\nregistry.category(\"services\").add(\"discuss.core.web\", discussCoreWeb);\n", "import { ChannelSelector } from \"@mail/discuss/core/web/channel_selector\";\nimport { onExternalClick } from \"@mail/utils/common/hooks\";\n\nimport { patch } from \"@web/core/utils/patch\";\nimport {\n    DiscussSidebarCategory,\n    DiscussSidebarChannel,\n} from \"../public_web/discuss_sidebar_categories\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useEffect, useState } from \"@odoo/owl\";\n\nDiscussSidebarCategory.components = { ...DiscussSidebarCategory.components, ChannelSelector };\n\n/** @type {import(\"@mail/discuss/core/public_web/discuss_sidebar_categories\").DiscussSidebarChannel} */\nconst DiscussSidebarChannelPatch = {\n    setup() {\n        super.setup();\n        this.actionService = useService(\"action\");\n    },\n    get commands() {\n        const commands = super.commands;\n        if (this.thread.channel_type === \"channel\") {\n            commands.push({\n                onSelect: () => this.openSettings(),\n                label: _t(\"Channel settings\"),\n                icon: \"fa fa-cog\",\n                sequence: 10,\n            });\n        }\n        return commands;\n    },\n    openSettings() {\n        if (this.thread.channel_type === \"channel\") {\n            this.actionService.doAction({\n                type: \"ir.actions.act_window\",\n                res_model: \"discuss.channel\",\n                res_id: this.thread.id,\n                views: [[false, \"form\"]],\n                target: \"current\",\n            });\n        }\n    },\n};\n\n/** @type {import(\"@mail/discuss/core/public_web/discuss_sidebar_categories\").DiscussSidebarCategory} */\nconst DiscussSidebarCategoryPatch = {\n    setup() {\n        super.setup();\n        this.actionService = useService(\"action\");\n        this.state ??= useState({});\n        this.state.editing = false;\n        onExternalClick(\"selector\", () => (this.state.editing = false));\n        useEffect(\n            () => {\n                if (this.store.discuss.isSidebarCompact && !this.floating.isOpen) {\n                    this.state.editing = false;\n                }\n            },\n            () => [this.floating.isOpen]\n        );\n    },\n    addToCategory() {\n        this.state.editing = true;\n    },\n    open() {\n        if (this.category.id === \"channels\") {\n            this.actionService.doAction({\n                name: _t(\"Public Channels\"),\n                type: \"ir.actions.act_window\",\n                res_model: \"discuss.channel\",\n                views: [\n                    [false, \"kanban\"],\n                    [false, \"form\"],\n                ],\n                domain: [\n                    [\"channel_type\", \"=\", \"channel\"],\n                    [\"parent_channel_id\", \"=\", false],\n                ],\n            });\n        }\n    },\n    onHover() {\n        if (this.state.editing && this.store.discuss.isSidebarCompact) {\n            return;\n        }\n        super.onHover(...arguments);\n        if (this.store.discuss.isSidebarCompact && !this.floating.isOpen) {\n            this.state.editing = false;\n        }\n    },\n    stopEditing() {\n        this.state.editing = false;\n    },\n    get actions() {\n        const actions = super.actions;\n        if (this.category.canView) {\n            actions.push({\n                onSelect: () => this.open(),\n                label: _t(\"View or join channels\"),\n                icon: \"fa fa-cog\",\n            });\n        }\n        if (this.category.canAdd && this.category.open) {\n            actions.push({\n                onSelect: () => this.addToCategory(),\n                label: this.category.addTitle,\n                icon: \"fa fa-plus\",\n                hotkey: this.category.addHotkey,\n                class: \"o-mail-DiscussSidebarCategory-add\",\n            });\n        }\n        return actions;\n    },\n};\n\npatch(DiscussSidebarChannel.prototype, DiscussSidebarChannelPatch);\npatch(DiscussSidebarCategory.prototype, DiscussSidebarCategoryPatch);\n", "import { MessagingMenu } from \"@mail/core/public_web/messaging_menu\";\nimport { ChannelSelector } from \"@mail/discuss/core/web/channel_selector\";\nimport { patch } from \"@web/core/utils/patch\";\n\nObject.assign(MessagingMenu.components, { ChannelSelector });\n\npatch(MessagingMenu.prototype, {\n    beforeOpen() {\n        const res = super.beforeOpen(...arguments);\n        this.store.channels.fetch();\n        return res;\n    },\n    onClickNewMessage() {\n        if (this.ui.isSmall || this.env.inDiscussApp) {\n            Object.assign(this.state, { adding: \"chat\" });\n        } else {\n            this.store.openNewMessage();\n            this.dropdown.close();\n        }\n    },\n    get counter() {\n        const count = super.counter;\n        const channelsContribution =\n            this.store.channels.status !== \"fetched\"\n                ? this.store.initChannelsUnreadCounter\n                : Object.values(this.store.Thread.records).filter(\n                      (thread) =>\n                          thread.displayToSelf &&\n                          !thread.isMuted &&\n                          (thread.selfMember?.message_unread_counter ||\n                              thread.message_needaction_counter)\n                  ).length;\n        // Needactions are already counted in the super call, but we want to discard them for channel so that there is only +1 per channel.\n        const channelsNeedactionCounter = Object.values(this.store.Thread.records).reduce(\n            (acc, thread) => {\n                return (\n                    acc +\n                    (thread.model === \"discuss.channel\" ? thread.message_needaction_counter : 0)\n                );\n            },\n            0\n        );\n        return count + channelsContribution - channelsNeedactionCounter;\n    },\n});\n", "import { Store } from \"@mail/core/common/store_service\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {import(\"models\").Store} */\nconst StorePatch = {\n    setup() {\n        super.setup(...arguments);\n        this.initChannelsUnreadCounter = 0;\n    },\n    onStarted() {\n        super.onStarted();\n        if (this.discuss.isActive) {\n            this.channels.fetch();\n        }\n    },\n};\npatch(Store.prototype, StorePatch);\n", "import { threadActionsRegistry } from \"@mail/core/common/thread_actions\";\nimport { useComponent } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nthreadActionsRegistry.add(\"expand-discuss\", {\n    condition(component) {\n        return (\n            component.thread &&\n            component.props.chatWindow?.isOpen &&\n            component.thread.model === \"discuss.channel\" &&\n            !component.ui.isSmall\n        );\n    },\n    setup() {\n        const component = useComponent();\n        component.actionService = useService(\"action\");\n    },\n    icon: \"fa fa-fw fa-expand\",\n    name: _t(\"Open in Discuss\"),\n    shouldClearBreadcrumbs(component) {\n        return false;\n    },\n    open(component) {\n        component.actionService.doAction(\n            {\n                type: \"ir.actions.client\",\n                tag: \"mail.action_discuss\",\n            },\n            {\n                clearBreadcrumbs: this.shouldClearBreadcrumbs(component),\n                additionalContext: { active_id: component.thread.id },\n            }\n        );\n    },\n    sequence: 40,\n    sequenceGroup: 20,\n});\n", "import { Thread } from \"@mail/core/common/thread_model\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Thread.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.foldStateCount = 0;\n    },\n    onPinStateUpdated() {\n        super.onPinStateUpdated();\n        if (!this.displayToSelf && !this.isLocallyPinned && this.eq(this.store.discuss.thread)) {\n            if (this.store.discuss.isActive) {\n                const newThread =\n                    this.store.discuss.channels.threads.find(\n                        (thread) => thread.displayToSelf || thread.isLocallyPinned\n                    ) || this.store.inbox;\n                newThread.setAsDiscussThread();\n            } else {\n                this.store.discuss.thread = undefined;\n            }\n        }\n    },\n});\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\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\n\nimport {\n    Component,\n    onMounted,\n    onPatched,\n    onWillUnmount,\n    toRaw,\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.isMobileOs = isMobileOS();\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        if (!activeSession) {\n            this.state.insetCard = undefined;\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        } else {\n            this.state.insetCard = undefined;\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        const key = \"session_\" + session.id;\n        if (toRaw(this.state).insetCard?.key === key) {\n            this.state.insetCard.type = videoType;\n            this.state.insetCard.videoStream = session.getStream(videoType);\n        } else {\n            this.state.insetCard = {\n                key,\n                session,\n                type: videoType,\n                videoStream: session.getStream(videoType),\n            };\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, { camera = false } = {}) {\n        await this.rtc.toggleCall(this.props.thread, { camera, fullscreen: this.props.fullscreen });\n    }\n}\n", "import { useComponent, useState } from \"@odoo/owl\";\nimport { isBrowserSafari, 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: 50,\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        activeClass: \"text-success\",\n        select: (component) => component.rtc.toggleVideo(\"screen\"),\n        sequence: 40,\n    })\n    .add(\"blur-background\", {\n        condition: (component) =>\n            !isBrowserSafari() && component.rtc && component.rtc.selfSession?.isCameraOn,\n        name: (component) =>\n            component.store.settings.useBlur ? _t(\"Remove Blur\") : _t(\"Blur Background\"),\n        isActive: (component) => component.store?.settings?.useBlur,\n        icon: \"fa-photo\",\n        select: (component) => {\n            component.store.settings.useBlur = !component.store.settings.useBlur;\n        },\n        sequence: 60,\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: 70,\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                if (!uploadStats || !downloadStats) {\n                    return;\n                }\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, onWillUnmount, useRef, useState } 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        this.ui = useState(useService(\"ui\"));\n        this.state = useState({ videoStream: null });\n        this.videoRef = useRef(\"video\");\n        onWillUnmount(() => {\n            if (!this.state.videoStream) {\n                return;\n            }\n            this.stopTracksOnMediaStream(this.state.videoStream);\n        });\n    }\n\n    async onClickAccept(ev, { camera = false } = {}) {\n        this.props.thread.open();\n        if (this.rtc.state.hasPendingRequest) {\n            return;\n        }\n        await this.rtc.toggleCall(this.props.thread, { camera });\n    }\n\n    onClickAvatar(ev) {\n        this.props.thread.open();\n    }\n\n    get hasRtcSupport() {\n        return Boolean(\n            navigator.mediaDevices && navigator.mediaDevices.getUserMedia && window.MediaStream\n        );\n    }\n\n    onClickPreviewCamera() {\n        this.enableVideo();\n    }\n\n    async enableVideo() {\n        if (!this.hasRtcSupport) {\n            return;\n        }\n        try {\n            this.state.videoStream = await navigator.mediaDevices.getUserMedia({ video: true });\n            this.videoRef.el.srcObject = this.state.videoStream;\n        } catch {\n            // TODO: display popup asking the user to re-enable their camera\n        }\n    }\n\n    /** @param {MediaStream} mediaStream */\n    stopTracksOnMediaStream(mediaStream) {\n        if (!mediaStream) {\n            return;\n        }\n        for (const track of mediaStream.getTracks()) {\n            track.stop();\n        }\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\nexport const callInvitationsService = {\n    dependencies: [\"discuss.rtc\", \"mail.store\"],\n    start() {\n        registry\n            .category(\"main_components\")\n            .add(\"discuss.CallInvitations\", { Component: CallInvitations });\n    },\n};\nregistry.category(\"services\").add(\"discuss.call_invitations\", callInvitationsService);\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        this.isEnterprise = odoo.info && odoo.info.isEnterprise;\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.dragPos = undefined;\n        this.isDrag = false;\n        this.parentBoundingRect = 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        if (\n            !this.rtcSession ||\n            !this.isOfActiveCall ||\n            HIDDEN_CONNECTION_STATES.has(this.rtcSession.connectionState)\n        ) {\n            return false;\n        }\n        if (this.rtc.state.connectionType === CONNECTION_TYPES.SERVER) {\n            return this.rtcSession.eq(this.rtc?.selfSession);\n        } else {\n            return this.rtcSession.notEq(this.rtc?.selfSession);\n        }\n    }\n\n    /**\n     * @deprecated use `showConnectionState` instead\n     */\n    get showServerState() {\n        return false;\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(\n            this.rtcSession && this.rtcSession.isActuallyTalking && !this.rtc.selfSession?.isDeaf\n        );\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.isDrag) {\n            this.isDrag = 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            const bottomOffset = this.env.inChatWindow ? window.innerHeight * 0.05 : 0; // 5vh in pixels\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 (\n                parseInt(insetEl.style.top) <\n                (insetEl.parentNode.offsetHeight - bottomOffset) / 2\n            ) {\n                insetEl.style.top = \"1vh\";\n                insetEl.style.bottom = \"\";\n            } else {\n                insetEl.style.bottom = this.env.inChatWindow ? \"5vh\" : \"1vh\";\n                insetEl.style.top = \"unset\";\n            }\n            this.dragPos = undefined;\n            this.parentBoundingRect = undefined;\n            document.removeEventListener(\"mouseup\", onMouseup);\n            document.removeEventListener(\"mousemove\", onMousemove);\n        };\n        document.addEventListener(\"mouseup\", onMouseup);\n        document.addEventListener(\"mousemove\", onMousemove);\n    }\n\n    onTouchMove(ev) {\n        if (!this.props.inset) {\n            return;\n        }\n        this.drag(ev);\n    }\n\n    drag(ev) {\n        this.isDrag = true;\n        const insetEl = this.root.el;\n        const parent = insetEl.parentNode;\n        const boundingRect =\n            this.parentBoundingRect || (this.parentBoundingRect = parent.getBoundingClientRect());\n        const bottomOffset = this.env.inChatWindow ? window.innerHeight * 0.05 : 0; // 5vh in pixels\n        const clientX = Math.max((ev.clientX ?? ev.touches[0].clientX) - boundingRect.left, 0);\n        const clientY = Math.max((ev.clientY ?? ev.touches[0].clientY) - boundingRect.top, 0);\n        if (!this.dragPos) {\n            this.dragPos = { posX: clientX, posY: clientY };\n        }\n        const dX = this.dragPos.posX - clientX;\n        const dY = this.dragPos.posY - clientY;\n        const widthOffset = parent.offsetWidth - insetEl.clientWidth;\n        const heightOffset = parent.offsetHeight - insetEl.clientHeight - bottomOffset;\n        this.dragPos.posX = Math.min(clientX, widthOffset);\n        this.dragPos.posY = Math.min(clientY, heightOffset);\n        insetEl.style.left = Math.min(Math.max(insetEl.offsetLeft - dX, 0), widthOffset) + \"px\";\n        insetEl.style.top = Math.min(Math.max(insetEl.offsetTop - dY, 0), heightOffset) + \"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        this.rtc.logSnapshot();\n        const data = JSON.stringify(this.rtc.state.globalLogs);\n        const blob = new Blob([data], { type: \"application/json\" });\n        const downloadLink = document.createElement(\"a\");\n        const now = luxon.DateTime.now().toFormat(\"yyyy-ll-dd_HH-mm\");\n        downloadLink.download = `RtcLogs_${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 { Deferred } from \"@web/core/utils/concurrency\";\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 Deferred();\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.ready.resolve?.();\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    get isActive() {\n        return Boolean(this.selfId !== undefined && this.channelId !== undefined);\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.WARN,\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._isPendingNotify = false;\n        this._notificationsToSend.clear();\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(peer.ready.then(() => 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                if (!this.isActive) {\n                    return;\n                }\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        this._emitLog(id, `connection recovery candidate: ${reason}`, LOG_LEVEL.WARN);\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                const connectionSuccess =\n                    peer.connection.connectionState === \"connected\" ||\n                    peer.connection.connectionState === \"completed\";\n                const iceSuccess =\n                    peer.connection.iceConnectionState === \"connected\" ||\n                    peer.connection.iceConnectionState === \"completed\";\n                if (!peer?.connection || !this.channelId || (connectionSuccess && iceSuccess)) {\n                    return;\n                }\n                this._emitLog(id, `attempting to recover connection: ${reason}`, LOG_LEVEL.ERROR);\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        if (!this.isActive) {\n            this._isPendingNotify = false;\n            return;\n        }\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._emitUpdate({\n            name: UPDATE_EVENT.CONNECTION_CHANGE,\n            payload: { id, peer, state: \"searching for network\" },\n        });\n        this.peers.set(id, peer);\n        peerConnection.addEventListener(\"icecandidate\", async (event) => {\n            if (!event.candidate) {\n                return;\n            }\n            if (!this.isActive) {\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            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._emitUpdate({\n                name: UPDATE_EVENT.CONNECTION_CHANGE,\n                payload: { id, peer, state: peerConnection.connectionState },\n            });\n            switch (peerConnection.connectionState) {\n                case \"closed\":\n                    this.removePeer(id);\n                    break;\n                case \"failed\":\n                case \"disconnected\":\n                    this._recover(peer.id, 1000, \"connection disconnected\");\n                    break;\n            }\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            if (!this.isActive) {\n                return;\n            }\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            if (!peer?.id || !this.peers.has(peer.id)) {\n                return;\n            }\n            const streamType = peer.getTransceiverStreamType(transceiver);\n            if (!streamType) {\n                this._recover(id, \"received track for unknown transceiver\");\n                return;\n            }\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            peer.ready.resolve();\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, loadJS } from \"@web/core/assets\";\nimport { memoize } from \"@web/core/utils/functions\";\nimport { url } from \"@web/core/utils/urls\";\nimport { callActionsRegistry } from \"./call_actions\";\n\n/**\n *\n * @param {EventTarget} target\n * @param {string} event\n * @param {Function} f event listener callback\n * @return {Function} unsubscribe function\n */\nfunction subscribe(target, event, f) {\n    target.addEventListener(event, f);\n    return () => target.removeEventListener(event, f);\n}\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 {[{ 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    getSfuConsumerStats(sessionId) {\n        const consumers = this.sfu?._consumers.get(sessionId);\n        if (!consumers) {\n            return [];\n        }\n        return Object.entries(consumers).map(([type, consumer]) => {\n            let state = \"active\";\n            if (!consumer) {\n                state = \"no consumer\";\n            } else if (consumer.closed) {\n                state = \"closed\";\n            } else if (consumer.paused) {\n                state = \"paused\";\n            } else if (!consumer.track) {\n                state = \"no track\";\n            } else if (!consumer.track.enabled) {\n                state = \"track disabled\";\n            } else if (consumer.track.muted) {\n                state = \"track muted\";\n            }\n            return { type, state };\n        });\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    removeSfu() {\n        if (!this.sfu) {\n            return;\n        }\n        for (const { name, f } of this._listeners) {\n            this.sfu.removeEventListener(name, f);\n        }\n        this.sfu.disconnect();\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 Promise.all([\n            this.p2p.updateUpload(type, track),\n            this.sfu?.updateUpload(type, track),\n        ]);\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    /** callbacks to be called when cleaning the state up after a call */\n    cleanups = [];\n    /** @type {number} */\n    sfuTimeout;\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            globalLogs: {},\n            logs: new Map(), // deprecated\n            sendCamera: false,\n            sendScreen: false,\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             * Whether the network fell back to p2p mode in a SFU call.\n             */\n            fallbackMode: false,\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\");\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\");\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        this.logSnapshot();\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 {{ exit: () => {} }} [initialState.fullscreen] if set, the call view is using fullscreen.\n     *   Providing fullscreen object allows to exit on call leave.\n     * @param {boolean} [initialState.camera]\n     */\n    async toggleCall(channel, { audio = true, fullscreen, camera } = {}) {\n        await Promise.resolve(() =>\n            loadJS(url(\"/mail/static/lib/selfie_segmentation/selfie_segmentation.js\")).catch(\n                () => {}\n            )\n        );\n        if (this.state.hasPendingRequest) {\n            return;\n        }\n        const isActiveCall = channel.eq(this.state.channel);\n        if (this.state.channel) {\n            fullscreen?.exit();\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    updateUpload() {\n        this.network?.updateUpload(\"audio\", this.state.audioTrack);\n        this.network?.updateUpload(\"camera\", this.state.cameraTrack);\n        this.network?.updateUpload(\"screen\", this.state.screenTrack);\n    }\n\n    async _initConnection() {\n        this.selfSession.connectionState = \"selecting network type\";\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        this.updateUpload();\n        if (this.serverInfo) {\n            this.log(this.selfSession, \"loading sfu server\", {\n                step: \"loading sfu server\",\n                serverInfo: this.serverInfo,\n            });\n            this.selfSession.connectionState = \"loading SFU assets\";\n            try {\n                await this._loadSfu();\n                this.state.connectionType = CONNECTION_TYPES.SERVER;\n                if (this.network) {\n                    this.network.addSfu(this.sfuClient);\n                } else {\n                    return; // the call may be ended by the time the sfu is loaded\n                }\n            } catch (e) {\n                this.state.fallbackMode = true;\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\", {\n                    error: e,\n                    important: true,\n                });\n            }\n            this.selfSession.connectionState = \"initializing\";\n        } else {\n            this.log(this.selfSession, \"no sfu server info, using peer-to-peer\");\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, important: true });\n            }\n        });\n        if (this.state.channel) {\n            await this.call();\n            this.updateUpload();\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     * @param {Boolean} [param2.important] if the log is important and should be kept even if logRtc is disabled\n     */\n    log(session, entry, { error, step, state, important, ...data } = {}) {\n        session.logStep = entry;\n        if (!this.store.settings.logRtc && !important) {\n            return;\n        }\n        let sessionEntry = this.state.logs[session.id];\n        if (!sessionEntry) {\n            this.state.logs[session.id] = sessionEntry = { step: \"\", state: \"\", logs: [] };\n        }\n        if (step) {\n            sessionEntry.step = step;\n        }\n        if (state) {\n            sessionEntry.state = state;\n        }\n        sessionEntry.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            ...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 = await this.store.RtcSession.getWhenReady(Number(id));\n                    if (!session || !this.state.channel) {\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 = await this.store.RtcSession.getWhenReady(sessionId);\n                    if (!session || !this.state.channel) {\n                        this.log(\n                            this.selfSession,\n                            `track received for unknown session ${sessionId} (${this.state.connectionType})`\n                        );\n                        return;\n                    }\n                    this.log(session, `${type} track received (${this.state.connectionType})`);\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    _handleSfuClientStateChange({ detail: { state, cause } }) {\n        this.log(this.selfSession, `sfu state change: ${state}. cause: ${cause}`, { state });\n        this.selfSession.connectionState = 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                break;\n            case this.SFU_CLIENT_STATE.CONNECTED:\n                browser.clearTimeout(this.sfuTimeout);\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                return;\n            case this.SFU_CLIENT_STATE.CLOSED:\n                {\n                    if (!this.state.channel) {\n                        return;\n                    }\n                    let text;\n                    if (cause === \"full\") {\n                        text = _t(\"Channel full\");\n                        this.leaveCall();\n                    } else {\n                        text = _t(\n                            \"Connection to SFU server closed by the server, falling back to peer-to-peer\"\n                        );\n                        this.log(this.selfSession, text, { important: true });\n                        this._downgradeConnection();\n                    }\n                    this.notification.add(text, {\n                        type: \"warning\",\n                    });\n                }\n                return;\n        }\n    }\n\n    async _downgradeConnection() {\n        this.serverInfo = undefined;\n        this.state.fallbackMode = true;\n        this.state.connectionType = CONNECTION_TYPES.P2P;\n        this.network.removeSfu();\n        await this.call();\n        this.updateUpload();\n    }\n\n    /**\n     *\n     * @param {Object} [param0={}]\n     * @param {boolean} [param0.asFallback=false] whether the call is made as a fallback to the SFU, in which case\n     * p2p connections are offered more eagerly as other participants may not offer them if their primary connection\n     * type is SFU.\n     * @return {Promise<void>}\n     */\n    async call({ asFallback = false } = {}) {\n        if (asFallback && !this.state.fallbackMode) {\n            return;\n        }\n        if (this.state.connectionType === CONNECTION_TYPES.SERVER) {\n            if (this.sfuClient.state === this.SFU_CLIENT_STATE.DISCONNECTED) {\n                browser.clearTimeout(this.sfuTimeout);\n                this.sfuTimeout = browser.setTimeout(() => {\n                    this.log(this.selfSession, \"sfu connection timeout\", { important: true });\n                    this._downgradeConnection();\n                }, 10000);\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                camera,\n                channel_id: channel.id,\n                check_rtc_session_ids: channel.rtcSessions.map((session) => session.id),\n            },\n            { silent: true }\n        );\n        this.state.hasPendingRequest = false;\n        // Initializing a new session implies closing the current session.\n        this.clear();\n        this.state.channel = channel;\n        this.store.insert(data);\n        this.newLogs();\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        if (camera) {\n            await this.toggleVideo(\"camera\");\n        }\n        await this._initConnection();\n        await this.resetAudioTrack({ force: audio });\n        if (!this.state.channel?.id) {\n            return;\n        }\n        this.soundEffectsService.play(\"channel-join\");\n        this.cleanups.push(\n            // only register the beforeunload event if there is a call as FireFox will not place\n            // the pages with beforeunload listeners in the bfcache.\n            subscribe(browser, \"beforeunload\", (event) => {\n                event.preventDefault();\n            })\n        );\n    }\n\n    newLogs() {\n        const date = luxon.DateTime.now().toFormat(\"yyyy-MM-dd-HH:mm:ss\");\n        const id = `c:${this.state.channel.id}-s:${this.selfSession.id}-d:${date}`;\n        this.state.logs = this.state.globalLogs[id] = {};\n        this.state.logs[\"hasTurn\"] = hasTurn(this.iceServers);\n    }\n\n    logSnapshot() {\n        const peers = [];\n        this.p2pService?.peers.forEach((peer) => {\n            peers.push({\n                id: peer.id,\n                state: peer.connection.connectionState,\n                iceState: peer.connection.iceConnectionState,\n            });\n        });\n        const server = {};\n        if (this.state.connectionType === CONNECTION_TYPES.SERVER) {\n            server.info = this.serverInfo;\n            server.state = this.sfuClient?.state;\n            server.consumersStats = this.state.channel.rtcSessions.map((session) =>\n                this.network.getSfuConsumerStats(session.id)\n            );\n            server.errors = this.sfuClient?.errors.map((error) => error.message);\n        }\n        this.state.globalLogs[`snapshot-${luxon.DateTime.now().toFormat(\"yyyy-MM-dd-HH-mm-ss\")}`] =\n            {\n                peers,\n                server,\n                connectionType: this.state.connectionType,\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            this.cleanups.splice(0).forEach((cleanup) => cleanup());\n        }\n        browser.clearTimeout(this.sfuTimeout);\n        this.sfuClient = undefined;\n        this.network = 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        this.state.fallbackMode = undefined;\n        closeStream(this.state.sourceCameraStream);\n        this.state.sourceCameraStream = null;\n        closeStream(this.state.sourceScreenStream);\n        this.state.sourceScreenStream = 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            fallbackMode: false,\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, {\n                        type: \"camera\",\n                        cleanup: false,\n                    });\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, {\n                            type: \"screen\",\n                            cleanup: false,\n                        });\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        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            if (type === \"camera\" && this.blurManager) {\n                this.blurManager.close();\n                this.blurManager = undefined;\n            }\n            stopVideo();\n            return;\n        }\n        let sourceStream;\n        try {\n            if (type === \"camera\") {\n                if (this.state.sourceCameraStream) {\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) {\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 outputTrack = sourceStream ? sourceStream.getVideoTracks()[0] : undefined;\n        if (outputTrack) {\n            outputTrack.addEventListener(\"ended\", async () => {\n                await this.toggleVideo(type, false);\n            });\n        }\n        if (this.store.settings.useBlur && type === \"camera\") {\n            try {\n                this.blurManager?.close();\n                this.blurManager = new BlurManager(sourceStream, {\n                    backgroundBlur: this.store.settings.backgroundBlurAmount,\n                    edgeBlur: this.store.settings.edgeBlurAmount,\n                });\n                const bluredStream = await this.blurManager.stream;\n                outputTrack = bluredStream.getVideoTracks()[0];\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        switch (type) {\n            case \"camera\": {\n                Object.assign(this.state, {\n                    sourceCameraStream: sourceStream,\n                    cameraTrack: outputTrack,\n                    sendCamera: Boolean(outputTrack),\n                });\n                break;\n            }\n            case \"screen\": {\n                Object.assign(this.state, {\n                    sourceScreenStream: sourceStream,\n                    screenTrack: outputTrack,\n                    sendScreen: Boolean(outputTrack),\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.log(this.selfSession, \"self session deleted, ending call\", {\n                    important: true,\n                });\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 {import(\"models\").RtcSession} session\n     * @param {Object} [param1]\n     * @param {String} [param1.type]\n     * @param {boolean} [param1.cleanup]\n     */\n    removeVideoFromSession(session, { type, cleanup = true } = {}) {\n        if (type) {\n            this.updateActiveSession(session, type);\n            if (cleanup) {\n                closeStream(session.videoStreams.get(type));\n            }\n            session.videoStreams.delete(type);\n        } else {\n            if (cleanup) {\n                for (const stream of session.videoStreams.values()) {\n                    closeStream(stream);\n                }\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                    session.mainVideoStreamType = \"camera\";\n                } else if (\n                    this.actionsStack.includes(\"camera-on\") &&\n                    this.actionsStack.includes(\"share-screen\")\n                ) {\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?.channelUUID === serverInfo.channelUUID) {\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            }\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\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\n\nexport class RtcSession extends Record {\n    static id = \"id\";\n    /** @type {Object.<number, import(\"models\").RtcSession>} */\n    static records = {};\n    static awaitedRecords = new Map();\n    /** @returns {import(\"models\").RtcSession} */\n    static get(data) {\n        return super.get(data);\n    }\n    /** @returns {Promies<import(\"models\").RtcSession>} */\n    static async getWhenReady(id) {\n        const session = this.get(id);\n        if (!session) {\n            let deferred = this.awaitedRecords.get(id);\n            if (!deferred) {\n                deferred = new Deferred();\n                this.awaitedRecords.set(id, deferred);\n                setTimeout(() => {\n                    deferred.resolve();\n                    this.awaitedRecords.delete(id);\n                }, 120_000);\n            }\n            return deferred;\n        }\n        return session;\n    }\n    /** @returns {import(\"models\").RtcSession} */\n    static new() {\n        const record = super.new(...arguments);\n        this.awaitedRecords.get(record.id)?.resolve(record);\n        this.awaitedRecords.delete(record.id);\n        return record;\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.persona.type === \"partner\" &&\n                        volume.persona.id === rtcSession.partnerId) ||\n                    (volume.persona.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(\"camera-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-video-camera\",\n        iconLarge: \"fa fa-fw fa-lg fa-video-camera\",\n        name: _t(\"Start a Video Call\"),\n        open(component) {\n            component.rtc.toggleCall(component.thread, { camera: true });\n        },\n        sequence: 5,\n        sequenceQuick: (component) => (component.env.inDiscussApp ? 25 : 35),\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\";\nimport { browser } from \"@web/core/browser/browser\";\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        /** @type {number|undefined} */\n        this.cancelRtcInvitationTimeout;\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                this.cancelRtcInvitationTimeout = browser.setTimeout(() => {\n                    this.store.env.services[\"discuss.rtc\"].leaveCall(this);\n                }, 30000);\n            },\n            /** @this {import(\"models\").Thread} */\n            onDelete(r) {\n                browser.clearTimeout(this.cancelRtcInvitationTimeout);\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                    this.store.rtc.call({ asFallback: true });\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 { markEventHandled } from \"@web/core/utils/misc\";\n\nimport { useRef, useState } from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {Composer} */\nconst composerPatch = {\n    setup() {\n        this.gifButton = useRef(\"gif-button\");\n        super.setup();\n        this.ui = useState(useService(\"ui\"));\n    },\n    get pickerSettings() {\n        const setting = super.pickerSettings;\n        if (this.hasGifPicker) {\n            setting.pickers.gif = (gif) => this.sendGifMessage(gif);\n            if (this.hasGifPickerButton) {\n                setting.buttons.push(this.gifButton);\n            }\n        }\n        return setting;\n    },\n    get hasGifPicker() {\n        return (\n            (this.store.hasGifPickerFeature || this.store.self.isAdmin) &&\n            !this.env.inChatter &&\n            !this.props.composer.message\n        );\n    },\n    get hasGifPickerButton() {\n        return this.hasGifPicker && !this.ui.isSmall && !this.env.inChatWindow;\n    },\n    onClickAddGif(ev) {\n        markEventHandled(ev, \"Composer.onClickAddGif\");\n    },\n    async sendGifMessage(gif) {\n        await this._sendMessage(gif.url, {\n            parentId: this.props.messageToReplyTo?.message?.id,\n        });\n    },\n};\npatch(Composer.prototype, composerPatch);\n", "import { Component, onWillStart, useState, useEffect } from \"@odoo/owl\";\nimport { useOnBottomScrolled, useSequential } from \"@mail/utils/common/hooks\";\nimport { user } from \"@web/core/user\";\nimport { useService, useAutofocus } from \"@web/core/utils/hooks\";\nimport { useDebounced } from \"@web/core/utils/timing\";\nimport { rpc } from \"@web/core/network/rpc\";\n\n/**\n * @typedef {Object} TenorCategory\n * @property {string} searchterm\n * @property {string} path\n * @property {string} image\n * @property {string} name\n */\n\n/**\n * @typedef {Object} TenorMediaFormat\n * @property {string} url\n * @property {number} duration\n * @property {string} preview\n * @property {number[]} dims\n * @property {number} size\n */\n\n/**\n * @typedef {Object} TenorGif\n * @property {string} id\n * @property {string} title\n * @property {number} created\n * @property {string} content_description\n * @property {string} itemurl\n * @property {string} url\n * @property {string[]} tags\n * @property {string[]} flags\n * @property {boolean} hasaudio\n * @property {{ tinygif: TenorMediaFormat }} media_formats\n */\n\n/**\n * @typedef {Object} Props\n * @property {function} onSelect Callback to use when the gif is selected\n * @property {string} [className]\n * @property {function} [close]\n * @property {Object} [state]\n * @extends {Component<Props, Env>}\n */\n\nexport class GifPicker extends Component {\n    static template = \"discuss.GifPicker\";\n    static props = [\"PICKERS?\", \"className?\", \"close?\", \"onSelect\", \"state?\"];\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.store = useState(useService(\"mail.store\"));\n        this.sequential = useSequential();\n        useAutofocus();\n        useOnBottomScrolled(\n            \"scroller\",\n            () => {\n                if (!this.state.showCategories) {\n                    this.state.loadingGif = true;\n                    if (!this.showFavorite) {\n                        this.search();\n                    } else {\n                        this.loadFavoritesDebounced(this.offset);\n                    }\n                }\n            },\n            300\n        );\n        this.next = \"\";\n        this.showFavorite = false;\n        this.offset = 0;\n        this.state = useState({\n            favorites: {\n                /** @type {TenorGif[]} */\n                gifs: [],\n                offset: 0,\n            },\n            searchTerm: \"\",\n            showCategories: true,\n            /** @type {TenorCategory[]} */\n            categories: [],\n            loadingGif: false,\n            loadingError: false,\n            evenGif: {\n                /** @type {Map<Number, TenorGif>} */\n                gifs: new Map(),\n                /** Size, in pixel, of the column. */\n                columnSize: 0,\n            },\n            oddGif: {\n                /** @type {Map<Number, TenorGif>} */\n                gifs: new Map(),\n                /** Size, in pixel, of the column. */\n                columnSize: 0,\n            },\n        });\n        this.loadFavoritesDebounced = useDebounced(this.loadFavorites, 200);\n        onWillStart(() => {\n            this.loadCategories();\n        });\n        if (this.store.self.type === \"partner\") {\n            onWillStart(() => {\n                this.loadFavorites();\n            });\n        }\n        useEffect(\n            () => {\n                if (this.props.state?.picker !== this.props.PICKERS?.GIF) {\n                    return;\n                }\n                this.clear();\n                this.state.loadingGif = true;\n                this.search();\n                if (this.searchTerm) {\n                    this.closeCategories();\n                } else {\n                    this.openCategories();\n                }\n            },\n            () => [this.searchTerm, this.props.state?.picker]\n        );\n    }\n\n    get style() {\n        return \"\";\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    async loadCategories() {\n        try {\n            let { language, region } = new Intl.Locale(user.lang);\n            if (!region && language === \"sr\") {\n                region = \"RS\";\n            }\n            const { tags } = await rpc(\n                \"/discuss/gif/categories\",\n                {\n                    country: region,\n                    locale: `${language}_${region}`,\n                },\n                { silent: true }\n            );\n            if (tags) {\n                this.state.categories = tags;\n            }\n        } catch {\n            this.state.loadingError = true;\n        }\n    }\n\n    openCategories() {\n        this.showFavorite = false;\n        this.state.showCategories = true;\n        this.searchTerm = \"\";\n        this.clear();\n    }\n\n    closeCategories() {\n        this.state.showCategories = false;\n    }\n\n    async search() {\n        if (!this.searchTerm) {\n            return;\n        }\n        try {\n            let { language, region } = new Intl.Locale(user.lang);\n            if (!region && language === \"sr\") {\n                region = \"RS\";\n            }\n            const params = {\n                country: region,\n                locale: `${language}_${region}`,\n                search_term: this.searchTerm,\n            };\n            if (this.next) {\n                params.position = this.next;\n            }\n            const res = await this.sequential(() => {\n                this.state.loadingGif = true;\n                const res = rpc(\"/discuss/gif/search\", params, {\n                    silent: true,\n                });\n                this.state.loadingGif = false;\n                return res;\n            });\n            if (res) {\n                const { next, results } = res;\n                this.next = next;\n                for (const gif of results) {\n                    this.pushGif(gif);\n                }\n                this.state.loadingError = false;\n            }\n        } catch {\n            this.state.loadingError = true;\n        }\n    }\n\n    /**\n     * @param {TenorGif} gif\n     */\n    pushGif(gif) {\n        if (this.state.evenGif.columnSize <= this.state.oddGif.columnSize) {\n            this.state.evenGif.gifs.set(gif.id, gif);\n            this.state.evenGif.columnSize += gif.media_formats.tinygif.dims[1];\n        } else {\n            this.state.oddGif.gifs.set(gif.id, gif);\n            this.state.oddGif.columnSize += gif.media_formats.tinygif.dims[1];\n        }\n    }\n\n    /**\n     * @param {TenorGif} gif\n     */\n    onClickGif(gif) {\n        this.props.onSelect(gif, true);\n        this.props.close?.();\n    }\n\n    clear() {\n        this.state.evenGif.gifs.clear();\n        this.state.evenGif.columnSize = 0;\n        this.state.oddGif.gifs.clear();\n        this.state.oddGif.columnSize = 0;\n    }\n\n    /**\n     * @param {TenorCategory} category\n     */\n    async onClickCategory(category) {\n        this.clear();\n        this.props.state.searchTerm = category.searchterm;\n        this.closeCategories();\n    }\n\n    /**\n     * @param {TenorGif} gif\n     */\n    async onClickFavorite(gif) {\n        if (!this.isFavorite(gif)) {\n            this.state.favorites.gifs.push(gif);\n            await this.orm.silent.create(\"discuss.gif.favorite\", [{ tenor_gif_id: gif.id }]);\n        } else {\n            const index = this.state.favorites.gifs.findIndex(({ id }) => id === gif.id);\n            if (index >= 0) {\n                this.state.favorites.gifs.splice(index, 1);\n            }\n            await rpc(\"/discuss/gif/remove_favorite\", { tenor_gif_id: gif.id }, { silent: true });\n        }\n    }\n\n    async loadFavorites() {\n        this.state.loadingGif = true;\n        try {\n            const [results] = await rpc(\n                \"/discuss/gif/favorites\",\n                { offset: this.offset },\n                { silent: true }\n            );\n            this.offset += 20;\n            this.state.favorites.gifs.push(...results);\n        } catch {\n            this.state.loadingError = true;\n        }\n        this.state.loadingGif = false;\n    }\n\n    /**\n     * @param {TenorGif} gif\n     */\n    isFavorite(gif) {\n        return this.state.favorites.gifs.map((favorite) => favorite.id).includes(gif.id);\n    }\n\n    onClickFavoritesCategory() {\n        this.showFavorite = true;\n        for (const gif of this.state.favorites.gifs) {\n            this.pushGif(gif);\n        }\n        this.closeCategories();\n    }\n}\n", "import { useState } from \"@odoo/owl\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { GifPicker } from \"@mail/discuss/gif_picker/common/gif_picker\";\nimport { PickerContent } from \"@mail/core/common/picker_content\";\n\nObject.assign(PickerContent.components, { GifPicker });\n\npatch(PickerContent.prototype, {\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n    },\n});\n", "import { patch } from \"@web/core/utils/patch\";\nimport { Picker } from \"@mail/core/common/picker\";\nimport { isEventHandled } from \"@web/core/utils/misc\";\n\npatch(Picker.prototype, {\n    /**\n     * @param {Event} ev\n     * @returns {boolean}\n     */\n    isEventHandledByPicker(ev) {\n        return super.isEventHandledByPicker(ev) || isEventHandled(ev, \"Composer.onClickAddGif\");\n    },\n    async toggle(el, ev) {\n        // Let event be handled by bubbling handlers first.\n        await super.toggle(el, ev);\n        if (isEventHandled(ev, \"Composer.onClickAddGif\")) {\n            if (this.popover.isOpen) {\n                if (this.props.state.picker === this.props.PICKERS.GIF) {\n                    this.close();\n                    return;\n                }\n                this.props.state.picker = this.props.PICKERS.GIF;\n            } else {\n                this.props.state.picker = this.props.PICKERS.GIF;\n                this.popover.open(el, this.contentProps);\n            }\n        }\n    },\n});\n", "import { Store } from \"@mail/core/common/store_service\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {import(\"models\").Store} */\nconst StorePatch = {\n    setup() {\n        super.setup(...arguments);\n        this.hasGifPickerFeature = false;\n    },\n};\npatch(Store.prototype, StorePatch);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { messageActionsRegistry } from \"@mail/core/common/message_actions\";\n\nmessageActionsRegistry.add(\"pin\", {\n    condition: (component) =>\n        component.store.self.type === \"partner\" &&\n        component.props.thread?.model === \"discuss.channel\",\n    icon: \"fa fa-thumb-tack\",\n    title: (component) => (component.props.message.pinned_at ? _t(\"Unpin\") : _t(\"Pin\")),\n    onClick: (component) => component.props.message.pin(),\n    sequence: 65,\n});\n", "import { patch } from \"@web/core/utils/patch\";\nimport { Message } from \"@mail/core/common/message_model\";\nimport { Record } from \"@mail/core/common/record\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { MessageConfirmDialog } from \"@mail/core/common/message_confirm_dialog\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\n\npatch(Message.prototype, {\n    setup() {\n        super.setup();\n        /** @type {luxon.DateTime} */\n        this.pinned_at = Record.attr(undefined, { type: \"datetime\" });\n    },\n    pin() {\n        if (this.pinned_at) {\n            return this.unpin();\n        }\n        const def = new Deferred();\n        this.store.env.services.dialog.add(\n            MessageConfirmDialog,\n            {\n                confirmText: _t(\"Yeah, pin it!\"),\n                message: this,\n                prompt: _t(\n                    \"You sure want this message pinned to %(conversation)s forever and ever?\",\n                    {\n                        conversation: this.thread.prefix + this.thread.displayName,\n                    }\n                ),\n                size: \"md\",\n                title: _t(\"Pin It\"),\n                onConfirm: () => {\n                    def.resolve(true);\n                    this.store.env.services.orm.call(\n                        \"discuss.channel\",\n                        \"set_message_pin\",\n                        [this.thread.id],\n                        { message_id: this.id, pinned: true }\n                    );\n                },\n            },\n            { onClose: () => def.resolve(false) }\n        );\n        return def;\n    },\n    unpin() {\n        const def = new Deferred();\n        this.store.env.services.dialog.add(\n            MessageConfirmDialog,\n            {\n                confirmColor: \"btn-danger\",\n                confirmText: _t(\"Yes, remove it please\"),\n                message: this,\n                prompt: _t(\n                    \"Well, nothing lasts forever, but are you sure you want to unpin this message?\"\n                ),\n                size: \"md\",\n                title: _t(\"Unpin Message\"),\n                onConfirm: () => {\n                    def.resolve(true);\n                    this.store.env.services.orm.call(\n                        \"discuss.channel\",\n                        \"set_message_pin\",\n                        [this.thread.id],\n                        { message_id: this.id, pinned: false }\n                    );\n                },\n            },\n            { onClose: () => def.resolve(false) }\n        );\n        return def;\n    },\n});\n", "import { Message } from \"@mail/core/common/message\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Message, {\n    components: { ...Message.components },\n});\n\npatch(Message.prototype, {\n    get isAlignedRight() {\n        return !this.env.messageCard && super.isAlignedRight;\n    },\n    get shouldDisplayAuthorName() {\n        if (this.env.messageCard) {\n            return true;\n        }\n        return super.shouldDisplayAuthorName;\n    },\n    /**\n     * @override\n     * @param {MouseEvent} ev\n     */\n    async onClickNotificationMessage(ev) {\n        const { oeType } = ev.target.dataset;\n        if (oeType === \"pin-menu\") {\n            this.env.pinMenu?.open();\n        }\n        await super.onClickNotificationMessage(...arguments);\n    },\n});\n", "import { MessageCardList } from \"@mail/core/common/message_card_list\";\nimport { ActionPanel } from \"@mail/discuss/core/common/action_panel\";\n\nimport { Component, onWillStart, onWillUpdateProps } from \"@odoo/owl\";\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 * @extends {Component<Props, Env>}\n */\nexport class PinnedMessagesPanel extends Component {\n    static components = {\n        MessageCardList,\n        ActionPanel,\n    };\n    static props = [\"thread\", \"className?\"];\n    static template = \"discuss.PinnedMessagesPanel\";\n\n    setup() {\n        super.setup();\n        onWillStart(() => {\n            this.props.thread.fetchPinnedMessages();\n        });\n        onWillUpdateProps((nextProps) => {\n            if (nextProps.thread.notEq(this.props.thread)) {\n                nextProps.thread.fetchPinnedMessages();\n            }\n        });\n    }\n\n    /**\n     * Get the message to display when nothing is pinned on this thread.\n     */\n    get emptyText() {\n        if (this.props.thread.channel_type === \"channel\") {\n            return _t(\"This channel doesn't have any pinned messages.\");\n        } else {\n            return _t(\"This conversation doesn't have any pinned messages.\");\n        }\n    }\n}\n", "import { threadActionsRegistry } from \"@mail/core/common/thread_actions\";\nimport { PinnedMessagesPanel } from \"@mail/discuss/message_pin/common/pinned_messages_panel\";\n\nimport { useChildSubEnv } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\n\nthreadActionsRegistry.add(\"pinned-messages\", {\n    component: PinnedMessagesPanel,\n    condition(component) {\n        return (\n            component.thread?.model === \"discuss.channel\" &&\n            (!component.props.chatWindow || component.props.chatWindow.isOpen)\n        );\n    },\n    panelOuterClass: \"o-discuss-PinnedMessagesPanel bg-inherit\",\n    icon: \"fa fa-fw fa-thumb-tack\",\n    iconLarge: \"fa fa-fw fa-lg fa-thumb-tack\",\n    name: _t(\"Pinned Messages\"),\n    nameActive: _t(\"Hide Pinned Messages\"),\n    sequence: 20,\n    sequenceGroup: 10,\n    setup(action) {\n        useChildSubEnv({\n            pinMenu: {\n                open: () => action.open(),\n                close: () => {\n                    if (action.isActive) {\n                        action.close();\n                    }\n                },\n            },\n        });\n    },\n    toggle: true,\n});\n", "import { patch } from \"@web/core/utils/patch\";\nimport { Record } from \"@mail/core/common/record\";\nimport { Thread } from \"@mail/core/common/thread_model\";\n\nimport { rpc } from \"@web/core/network/rpc\";\n\npatch(Thread.prototype, {\n    setup() {\n        super.setup();\n\n        /** @type {'loaded'|'loading'|'error'|undefined} */\n        this.pinnedMessagesState = undefined;\n        this.pinnedMessages = Record.many(\"Message\", {\n            compute() {\n                return this.allMessages.filter((m) => m.pinned_at);\n            },\n            sort: (m1, m2) => {\n                if (m1.pinned_at === m2.pinned_at) {\n                    return m1.id - m2.id;\n                }\n                return m1.pinned_at < m2.pinned_at ? 1 : -1;\n            },\n        });\n    },\n\n    /**\n     * @param {import(\"models\").Thread} channel\n     */\n    async fetchPinnedMessages() {\n        if (\n            this.model !== \"discuss.channel\" ||\n            [\"loaded\", \"loading\"].includes(this.pinnedMessagesState)\n        ) {\n            return;\n        }\n        this.pinnedMessagesState = \"loading\";\n        try {\n            const data = await rpc(\"/discuss/channel/pinned_messages\", {\n                channel_id: this.id,\n            });\n            this.store.insert(data, { html: true });\n            this.pinnedMessagesState = \"loaded\";\n        } catch (e) {\n            this.pinnedMessagesState = \"error\";\n            throw e;\n        }\n    },\n});\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 { onWillDestroy } from \"@odoo/owl\";\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        onWillDestroy(() => {\n            this.stopTyping();\n        });\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", "import { AttachmentList } from \"@mail/core/common/attachment_list\";\nimport { VoicePlayer } from \"@mail/discuss/voice_message/common/voice_player\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(AttachmentList, {\n    components: { ...AttachmentList.components, VoicePlayer },\n});\n", "import { Attachment } from \"@mail/core/common/attachment_model\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Attachment.prototype, {\n    get isViewable() {\n        return !this.voice && super.isViewable;\n    },\n    delete() {\n        if (this.voice && this.id > 0) {\n            this.store.env.services[\"discuss.voice_message\"].activePlayer = null;\n        }\n        super.delete(...arguments);\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    _makeAttachmentData(upload, tmpId, thread, tmpUrl) {\n        const attachmentData = super._makeAttachmentData(...arguments);\n        if (upload.data.get(\"voice\")) {\n            attachmentData.voice = upload.data.get(\"voice\");\n        }\n        return attachmentData;\n    },\n    _buildFormData(formData, tmpURL, thread, composer, tmpId, options) {\n        super._buildFormData(...arguments);\n        if (options?.voice) {\n            formData.append(\"voice\", true);\n        }\n        return formData;\n    },\n});\n", "import { Composer } from \"@mail/core/common/composer_model\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Composer.prototype, {\n    get voiceAttachment() {\n        return this.attachments.find((attachment) => attachment.voice);\n    },\n});\n", "import { Composer } from \"@mail/core/common/composer\";\nimport { VoiceRecorder } from \"@mail/discuss/voice_message/common/voice_recorder\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Composer, {\n    components: { ...Composer.components, VoiceRecorder },\n});\n\npatch(Composer.prototype, {\n    setup() {\n        super.setup();\n        this.state.recording = false;\n    },\n    get isSendButtonDisabled() {\n        return this.state.recording || super.isSendButtonDisabled;\n    },\n    onKeydown(ev) {\n        if (ev.key === \"Enter\" && this.state.recording) {\n            ev.preventDefault();\n            return;\n        }\n        return super.onKeydown(ev);\n    },\n});\n", "const MAX_SAMPLES = 1152;\n\nexport class Mp3Encoder {\n    /** @type {Object} */\n    config;\n    /** @type {boolean} */\n    encoding;\n    /** @type {lameJs.Mp3Encoder} */\n    mp3Encoder;\n    /** @type {Int16Array} */\n    samplesMono;\n\n    constructor(config = {}) {\n        this.config = {\n            sampleRate: 44100,\n            bitRate: 128,\n        };\n        Object.assign(this.config, config);\n        // eslint-disable-next-line no-undef\n        this.mp3Encoder = new lamejs.Mp3Encoder(1, this.config.sampleRate, this.config.bitRate);\n        this.samplesMono = null;\n        this.clearBuffer();\n    }\n\n    clearBuffer() {\n        this.dataBuffer = [];\n    }\n\n    appendToBuffer(buffer) {\n        this.dataBuffer.push(new Int8Array(buffer));\n    }\n\n    floatTo16BitPCM(input, output) {\n        for (let i = 0; i < input.length; i++) {\n            const s = Math.max(-1, Math.min(1, input[i]));\n            output[i] = s < 0 ? s * 0x8000 : s * 0x7fff;\n        }\n    }\n\n    convertBuffer(arrayBuffer) {\n        const data = new Float32Array(arrayBuffer);\n        const out = new Int16Array(arrayBuffer.length);\n        this.floatTo16BitPCM(data, out);\n        return out;\n    }\n\n    encode(arrayBuffer) {\n        this.encoding = true;\n        this.samplesMono = this.convertBuffer(arrayBuffer);\n        let remaining = this.samplesMono.length;\n        for (let i = 0; remaining >= 0; i += MAX_SAMPLES) {\n            const left = this.samplesMono.subarray(i, i + MAX_SAMPLES);\n            const mp3buffer = this.mp3Encoder.encodeBuffer(left);\n            this.appendToBuffer(mp3buffer);\n            remaining -= MAX_SAMPLES;\n        }\n    }\n\n    finish() {\n        if (this.encoding) {\n            this.appendToBuffer(this.mp3Encoder.flush());\n            return this.dataBuffer;\n        } else {\n            return [];\n        }\n    }\n}\n", "import { loadBundle } from \"@web/core/assets\";\nimport { registry } from \"@web/core/registry\";\nimport { memoize } from \"@web/core/utils/functions\";\n\nconst loader = {\n    loadLamejs: memoize(() => loadBundle(\"mail.assets_lamejs\")),\n};\n\nexport async function loadLamejs() {\n    try {\n        await loader.loadLamejs();\n    } catch {\n        // Could be intentional (tour ended successfully while lamejs still loading)\n    }\n}\n\nexport class VoiceMessageService {\n    constructor(env) {\n        /** @type {import(\"@mail/discuss/voice_message/common/voice_player\").VoicePlayer} */\n        this.activePlayer = null;\n    }\n}\n\nexport const voiceMessageService = {\n    start(env) {\n        return new VoiceMessageService(env);\n    },\n};\n\nregistry.category(\"services\").add(\"discuss.voice_message\", voiceMessageService);\n", "import {\n    Component,\n    useState,\n    onMounted,\n    onWillUnmount,\n    useEffect,\n    useRef,\n    status,\n} from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { url } from \"@web/core/utils/urls\";\n\nconst WAVE_COLOR = \"#7775\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Attachment} attachment\n * @extends {Component<Props, Env>}\n */\nexport class VoicePlayer extends Component {\n    static props = [\"attachment\"];\n    static template = \"mail.VoicePlayer\";\n\n    /** @type {number} */\n    lastPlaytime = 0;\n    /** @type {number} */\n    lastPos = 0;\n    /** @type {number} */\n    startPosition = 0;\n    /** @type {string} */\n    progressColor;\n    /** @type {GainNode} */\n    gainNode;\n    /** @type {AudioContext} */\n    audioCtx;\n    scheduledPause;\n    /** @type {AudioBuffer} */\n    buffer;\n    /** @type {AnalyserNode} */\n    analyser;\n    /** @type {AudioBufferSourceNode} */\n    source;\n    /** @type {number} */\n    width;\n    /** @type {number} */\n    height;\n    /** @type {HTMLElement} */\n    wrapper;\n    /** @type {HTMLElement} */\n    progressWave;\n    /** @type {CanvasRenderingContext2D} */\n    waveCtx;\n    /** @type {CanvasRenderingContext2D} */\n    progressCtx;\n\n    setup() {\n        super.setup();\n        this.wrapperRef = useRef(\"wrapper\");\n        this.drawerRef = useRef(\"drawer\");\n        this.waveRef = useRef(\"wave\");\n        this.progressRef = useRef(\"progress\");\n        /** @type {import(\"@mail/discuss/voice_message/common/voice_message_service\").VoiceMessageService} */\n        this.voiceMessageService = useService(\"discuss.voice_message\");\n        this.state = useState({\n            paused: true,\n            playing: false,\n            repeat: false,\n            visualTime: \"-- : --\",\n        });\n        useEffect(\n            (playing) => {\n                if (playing) {\n                    this.addOnAudioProcess();\n                }\n            },\n            () => [this.state.playing]\n        );\n        useEffect(\n            (uploading) => {\n                if (uploading) {\n                    return;\n                }\n                if (this.wasUploading && !uploading) {\n                    this.makeAudio();\n                }\n                this.wasUploading = uploading;\n            },\n            () => [this.props.attachment.uploading]\n        );\n        onMounted(() => {\n            this.initElements();\n            this.wrapper.addEventListener(\"click\", (e) => {\n                if (this.props.attachment.uploading) {\n                    return;\n                }\n                const clientX = (e.targetTouches ? e.targetTouches[0] : e).clientX;\n                const bcr = this.wrapper.getBoundingClientRect();\n                const progressPixels = clientX - bcr.left;\n                const progress = Math.min(\n                    Math.max(0, progressPixels / this.wrapper.scrollWidth),\n                    1\n                );\n                this.seekTo(progress);\n            });\n            if (!this.props.attachment.uploading) {\n                this.makeAudio();\n            }\n            this.wasUploading = this.props.attachment.uploading;\n        });\n        onWillUnmount(() => {\n            if (this.state.playing) {\n                this.pause();\n            }\n            this.destroyWebAudio();\n        });\n    }\n\n    makeAudio() {\n        this.audioCtx = new browser.AudioContext();\n        this.gainNode = this.audioCtx.createGain();\n        this.gainNode.connect(this.audioCtx.destination);\n        this.analyser = this.audioCtx.createAnalyser();\n        this.analyser.connect(this.gainNode);\n        this.fetchFile(\n            url(this.props.attachment.urlRoute, {\n                ...this.props.attachment.urlQueryParams,\n            })\n        ).then((arrayBuffer) => this.drawBuffer(arrayBuffer));\n    }\n\n    _fetch(...args) {\n        return fetch(...args);\n    }\n\n    async fetchFile(url) {\n        const response = await this._fetch(url);\n        if (!response.ok) {\n            throw new Error(\"HTTP error status: \" + response.status);\n        }\n        const arrayBuffer = await response.arrayBuffer();\n        return arrayBuffer;\n    }\n\n    getPlayedTime() {\n        return this.audioCtx.currentTime - this.lastPlaytime;\n    }\n\n    getCurrentTime() {\n        if (this.state.paused) {\n            return this.startPosition;\n        } else {\n            return this.startPosition + this.getPlayedTime();\n        }\n    }\n\n    play() {\n        if (this.voiceMessageService.activePlayer) {\n            this.voiceMessageService.activePlayer.pause();\n        }\n        this.voiceMessageService.activePlayer = this;\n        this.state.repeat = false;\n        this.createSource();\n        const { start, end } = this.seekToElapsed();\n        this.scheduledPause = end;\n        this.source.start(0, start);\n        this.state.playing = true;\n        this.state.paused = false;\n    }\n\n    pause(options) {\n        this.voiceMessageService.activePlayer = null;\n        if (options?.end) {\n            this.state.repeat = true;\n        }\n        this.scheduledPause = null;\n        this.startPosition += this.getPlayedTime();\n        if (this.source) {\n            try {\n                this.source.stop();\n            } catch (e) {\n                if (e.name === \"InvalidStateError\") {\n                    return;\n                }\n                throw e;\n            }\n        }\n        if (!options?.continue) {\n            this.state.paused = true;\n            this.state.playing = false;\n        }\n    }\n\n    getPeaks() {\n        const peaks = [];\n        const sampleSize = this.buffer.length / this.width;\n        const sampleStep = Math.floor(sampleSize / 10);\n        const chan = this.buffer.getChannelData(0);\n        let i;\n        for (i = 0; i < this.width; i++) {\n            const start = Math.floor(i * sampleSize);\n            const end = Math.floor(start + sampleSize);\n            let min = chan[start];\n            let max = min;\n            let j;\n            for (j = start; j < end; j += sampleStep) {\n                const value = chan[j];\n                if (value > max) {\n                    max = value;\n                }\n                if (value < min) {\n                    min = value;\n                }\n            }\n            peaks[i] = max;\n        }\n        return peaks;\n    }\n\n    createSource() {\n        this.source?.disconnect();\n        this.source = this.audioCtx.createBufferSource();\n        this.source.buffer = this.buffer;\n        this.source.connect(this.analyser);\n    }\n\n    /**\n     * @param {number} [start] float representing start time\n     * @param {number} [end] float representing end time\n     * @returns {Object} res\n     * @returns {number} res.start\n     * @returns {number} res.end\n     */\n    seekToElapsed(start, end) {\n        this.scheduledPause = null;\n        if (start === undefined) {\n            start = this.getCurrentTime();\n            if (start >= this.buffer.duration) {\n                start = 0;\n            }\n        }\n        if (end === undefined) {\n            end = this.buffer.duration;\n        }\n        this.startPosition = start;\n        this.lastPlaytime = this.audioCtx.currentTime;\n        return { start, end };\n    }\n\n    onProgress(progress) {\n        const position = Math.round(progress * this.width);\n        if (position < this.lastPos || position - this.lastPos >= 1) {\n            this.lastPos = position;\n            this.progressWave.style.width = position + \"px\";\n        }\n    }\n\n    seekTo(progress) {\n        if (this.state.playing) {\n            this.pause({ continue: true });\n        }\n        this.state.repeat = false;\n        const elapsedTime = progress * this.buffer.duration;\n        this.state.visualTime = this.generateTime(Math.floor(elapsedTime));\n        this.seekToElapsed(elapsedTime);\n        this.onProgress(progress);\n        if (this.state.playing) {\n            this.play();\n        }\n    }\n\n    async drawBuffer(arrayBuffer) {\n        const buffer = await this.audioCtx.decodeAudioData(arrayBuffer);\n        this.state.visualTime = this.generateTime(Math.floor(buffer.duration));\n        this.startPosition = 0;\n        this.lastPlaytime = this.audioCtx.currentTime;\n        this.buffer = buffer;\n        this.createSource();\n        this.drawWave(this.getPeaks());\n    }\n\n    async destroyWebAudio() {\n        this.source?.disconnect();\n        this.gainNode?.disconnect();\n        this.analyser?.disconnect();\n        try {\n            await this.audioCtx?.close();\n        } catch (e) {\n            if (e.name === \"InvalidStateError\") {\n                return;\n            }\n            throw e;\n        }\n    }\n\n    addOnAudioProcess() {\n        if (status(this) === \"destroyed\") {\n            return;\n        }\n        const time = this.getCurrentTime();\n        if (time >= this.scheduledPause && this.state.playing) {\n            this.pause({ end: true });\n        } else if (this.state.playing) {\n            this.state.visualTime = this.generateTime(Math.floor(time));\n            const playedPercents = this.getCurrentTime() / this.buffer.duration;\n            this.onProgress(playedPercents);\n            requestAnimationFrame(() => this.addOnAudioProcess());\n        }\n    }\n\n    generateTime(timeInSecond) {\n        const second = timeInSecond % 60;\n        const minute = Math.floor(timeInSecond / 60);\n        return (\n            (minute < 10 ? \"0\" + minute : minute) + \" : \" + (second < 10 ? \"0\" + second : second)\n        );\n    }\n\n    initElements() {\n        this.wrapper = this.wrapperRef.el;\n        this.progressWave = this.drawerRef.el;\n        this.progressColor = getComputedStyle(this.wrapper).getPropertyValue(\"--primary\");\n        this.width = this.wrapper.clientWidth;\n        this.height = this.wrapper.clientHeight;\n\n        const wave = this.waveRef.el;\n        wave.width = this.width;\n        wave.height = this.height;\n        this.waveCtx = wave.getContext(\"2d\");\n        this.waveCtx.fillStyle = WAVE_COLOR;\n\n        const progress = this.progressRef.el;\n        progress.width = this.width;\n        progress.height = this.height;\n        this.progressCtx = progress.getContext(\"2d\");\n        this.progressCtx.fillStyle = this.progressColor;\n    }\n\n    drawWave(peaks) {\n        return requestAnimationFrame(() => {\n            this.drawLines(peaks);\n            this.fillRect(0, this.height / 2, this.width, 0.5);\n        });\n    }\n\n    fillRect(x, y, width, height) {\n        const intersection = {\n            x1: x,\n            y1: y,\n            x2: x + width,\n            y2: y + height,\n        };\n        if (intersection.x1 < intersection.x2) {\n            this.fillRects(\n                intersection.x1,\n                intersection.y1,\n                intersection.x2 - intersection.x1,\n                intersection.y2 - intersection.y1\n            );\n        }\n    }\n\n    fillRects(x, y, width, height) {\n        this.waveCtx.fillRect(x, y, width, height);\n        this.progressCtx.fillRect(x, y, width, height);\n    }\n\n    drawLines(peaks) {\n        this.drawLineToContext(this.waveCtx, peaks);\n        this.drawLineToContext(this.progressCtx, peaks);\n    }\n\n    drawLineToContext(ctx, peaks) {\n        const maxPeak = Math.max(...peaks);\n        let i, peak;\n        for (i = 0; i <= peaks.length; i++) {\n            peak = peaks[i];\n            const h = (peak * this.height) / maxPeak;\n            ctx.fillRect(i, (this.height - h) / 2, 1.5, h);\n        }\n    }\n\n    onClickPlayPause() {\n        if (this.props.attachment.uploading) {\n            return;\n        }\n        if (this.state.paused) {\n            this.play();\n        } else {\n            this.pause();\n        }\n    }\n}\n", "import { Component, useState, onWillUnmount, status } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { Mp3Encoder } from \"./mp3_encoder\";\nimport { loadLamejs } from \"@mail/discuss/voice_message/common/voice_message_service\";\n\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Composer} composer\n * @property {function} [attachmentUploader]\n * @property {function} [onchangeRecording]\n * @extends {Component<Props, Env>}\n */\nexport class VoiceRecorder extends Component {\n    static props = [\"composer\", \"attachmentUploader\", \"onchangeRecording?\"];\n    static template = \"mail.VoiceRecorder\";\n\n    /** @type {MediaStream} */\n    microphone;\n    /** @type {number} */\n    startTimeStamp;\n    /** @type {AudioContext} */\n    audioContext;\n    /** @type {MediaStreamAudioSourceNode} */\n    streamSource;\n    /** @type {AudioWorkletNode} */\n    processor;\n    /** @type {Mp3Encoder} */\n    encoder;\n    /** @type {import(\"models\").Store} */\n    store;\n    /** @type {ReturnType<typeof import(\"@web/core/notifications/notification_service\").notificationService.start>} */\n    notification;\n    /** @type {Object} */\n    config;\n    /** @type {import(\"@mail/discuss/voice_message/common/voice_message_service\").VoiceMessageService} */\n    voiceMessageService;\n\n    setup() {\n        super.setup();\n        this.state = useState({\n            limitWarning: false,\n            isActionPending: false,\n            recording: false,\n            elapsed: \"00 : 00\",\n        });\n        this.notification = useService(\"notification\");\n        this.store = useState(useService(\"mail.store\"));\n        this.voiceMessageService = useState(useService(\"discuss.voice_message\"));\n        this.config = {\n            // 128 or 160 kbit/s \u2013 mid-range bitrate quality\n            bitRate: 128,\n        };\n        onWillUnmount(() => {\n            if (this.state.recording) {\n                this.notification.add(_t(\"Voice recording stopped\"), { type: \"warning\" });\n                this.stopRecording();\n            } else {\n                this.cleanUp({ unmounting: true });\n            }\n        });\n    }\n\n    filename() {\n        return (\n            \"Voice-\" +\n            new Date().toISOString().split(\"T\")[0] +\n            \"-\" +\n            Math.floor(Math.random() * 100000) +\n            \".mp3\"\n        );\n    }\n\n    async startRecording() {\n        if (this.state.isActionPending) {\n            return;\n        }\n        this.state.isActionPending = true;\n        if (!this.microphone) {\n            try {\n                this.microphone = await browser.navigator.mediaDevices.getUserMedia({\n                    audio: this.store.settings.audioConstraints,\n                });\n                if (status(this) === \"destroyed\") {\n                    this.cleanUp({ unmounting: true });\n                    return;\n                }\n            } catch {\n                this.notification.add(\n                    _t('\"%(hostname)s\" needs to access your microphone', {\n                        hostname: window.location.host,\n                    }),\n                    { type: \"warning\" }\n                );\n                this.state.isActionPending = false;\n                return;\n            }\n        }\n        this.state.elapsed = \"00 : 00\";\n        this.props.onchangeRecording?.();\n        this.state.recording = true;\n        this.audioContext = new browser.AudioContext();\n\n        await loadLamejs();\n        await this.audioContext.audioWorklet.addModule(\"/discuss/voice/worklet_processor\");\n        this.processor = new browser.AudioWorkletNode(this.audioContext, \"processor\");\n        this.processor.port.onmessage = (e) => {\n            if (this.state.recording && !this.startTimeStamp) {\n                this.startTimeStamp = e.timeStamp;\n            }\n            if (!this.startTimeStamp) {\n                return;\n            }\n            const elapsedSeconds = Math.floor((e.timeStamp - this.startTimeStamp) / 1000);\n            const second = elapsedSeconds % 60;\n            const minute = Math.floor(elapsedSeconds / 60);\n            this.state.elapsed =\n                (minute < 10 ? \"0\" + minute : minute) +\n                \" : \" +\n                (second < 10 ? \"0\" + second : second);\n            if (elapsedSeconds > 55 && elapsedSeconds < 60) {\n                this.state.limitWarning = true;\n            }\n            if (elapsedSeconds === 60) {\n                this.notification.add(\n                    _t(\"The duration of voice messages is limited to 1 minute.\"),\n                    { type: \"warning\" }\n                );\n                this.stopRecording();\n            }\n            if (!e.data) {\n                return;\n            }\n            this._encode(e.data);\n        };\n        this.streamSource = this.audioContext.createMediaStreamSource(this.microphone);\n\n        // Start to get microphone data\n        this.streamSource.connect(this.processor);\n        this.processor.connect(this.audioContext.destination);\n        this.config.sampleRate = this.audioContext.sampleRate;\n        this.encoder = new Mp3Encoder(this.config);\n        this.state.isActionPending = false;\n    }\n\n    _encode(data) {\n        this.encoder.encode(data);\n    }\n\n    _getEncoderBuffer() {\n        return this.encoder.finish();\n    }\n\n    _makeFile(buffer, type) {\n        return new File(buffer, this.filename(), { type });\n    }\n\n    stopRecording() {\n        this.getMp3()\n            .then((buffer) => {\n                const file = this._makeFile(buffer, \"audio/mp3\");\n                this.props.attachmentUploader.uploadFile(file, { voice: true });\n            })\n            .catch(() => {});\n        this.cleanUp();\n    }\n\n    cleanUp({ unmounting = false } = {}) {\n        if (this.processor && this.streamSource) {\n            // Clean up the Web Audio API resources.\n            this.streamSource.disconnect();\n            this.processor.disconnect();\n\n            if (this.audioContext && this.audioContext.state !== \"closed\") {\n                // If all references using this.audioContext are destroyed, context is\n                // closed automatically. DOMException is fired when trying to close again\n                this.audioContext.close();\n            }\n        }\n\n        this.startTimeStamp = false;\n        this.microphone?.getTracks().forEach((track) => track.stop());\n        this.microphone = null;\n        this.state.recording = false;\n        this.state.limitWarning = false;\n        if (!unmounting) {\n            this.props.onchangeRecording?.();\n        }\n    }\n\n    getMp3() {\n        const finalBuffer = this._getEncoderBuffer();\n        return new Promise((resolve, reject) => {\n            if (finalBuffer.length === 0) {\n                reject(new Error(\"No buffer to send\"));\n            } else {\n                resolve(finalBuffer);\n                this.encoder.clearBuffer();\n            }\n        });\n    }\n\n    onClick(ev) {\n        if (this.state.recording) {\n            this.stopRecording();\n        } else {\n            this.startRecording();\n        }\n    }\n}\n", "import { Thread } from \"@mail/core/common/thread_model\";\nimport { discussSidebarChannelIndicatorsRegistry } from \"@mail/discuss/core/public_web/discuss_sidebar_categories\";\n\nimport { Component, useState } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * @typedef {Object} Props\n * @property {import(models\").Thread} thread\n * @extends {Component<Props, Env>}\n */\nexport class DiscussSidebarCallIndicator extends Component {\n    static template = \"mail.DiscussSidebarCallIndicator\";\n    static props = { thread: { type: Thread } };\n    static components = {};\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n        this.rtc = useState(useService(\"discuss.rtc\"));\n    }\n}\n\ndiscussSidebarChannelIndicatorsRegistry.add(\"call-indicator\", DiscussSidebarCallIndicator);\n", "import { Component, useState } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Thread } from \"@mail/core/common/thread_model\";\nimport { callActionsRegistry } from \"../common/call_actions\";\nimport { useHover } from \"@mail/utils/common/hooks\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\n/**\n * @typedef {Object} Props\n * @property {import(\"models\").Thread} thread\n * @extends {Component<Props, Env>}\n */\nexport class DiscussSidebarCallParticipants extends Component {\n    static template = \"mail.DiscussSidebarCallParticipants\";\n    static props = { thread: { type: Thread }, compact: { type: Boolean, optional: true } };\n    static components = { DiscussSidebarCallParticipants, Dropdown };\n\n    setup() {\n        super.setup();\n        this.store = useState(useService(\"mail.store\"));\n        this.rtc = useState(useService(\"discuss.rtc\"));\n        this.hover = useHover([\"root\", \"floating*\"], {\n            onHover: () => (this.floating.isOpen = true),\n            onAway: () => (this.floating.isOpen = false),\n        });\n        this.floating = useDropdownState();\n    }\n\n    get callActionsRegistry() {\n        return callActionsRegistry;\n    }\n\n    get compact() {\n        if (typeof this.props.compact === \"boolean\") {\n            return this.props.compact;\n        }\n        return this.store.discuss.isSidebarCompact;\n    }\n\n    get lastActiveSession() {\n        const sessions = [...this.props.thread.rtcSessions];\n        sessions?.sort((s1, s2) => {\n            if (s1.isActuallyTalking && !s2.isActuallyTalking) {\n                return -1;\n            }\n            if (!s1.isActuallyTalking && s2.isActuallyTalking) {\n                return 1;\n            }\n            if (s1.isVideoStreaming && !s2.isVideoStreaming) {\n                return -1;\n            }\n            if (!s1.isVideoStreaming && s2.isVideoStreaming) {\n                return 1;\n            }\n            return s2.talkingTime - s1.talkingTime;\n        });\n        return sessions[0];\n    }\n\n    get sessions() {\n        const sessions = [...this.props.thread.rtcSessions];\n        return sessions.sort((s1, s2) => {\n            const persona1 = s1.channelMember?.persona;\n            const persona2 = s2.channelMember?.persona;\n            return (\n                persona1?.name?.localeCompare(persona2?.name) ||\n                s1.channelMember?.id - s2.channelMember?.id ||\n                s1.id - s2.id\n            );\n        });\n    }\n}\n", "import { DiscussSidebarCallParticipants } from \"@mail/discuss/call/public_web/discuss_sidebar_call_participants\";\nimport { DiscussSidebarChannel } from \"@mail/discuss/core/public_web/discuss_sidebar_categories\";\nimport { patch } from \"@web/core/utils/patch\";\n\nDiscussSidebarChannel.components = Object.assign(DiscussSidebarChannel.components || {}, {\n    DiscussSidebarCallParticipants,\n});\n\npatch(DiscussSidebarChannel.prototype, {\n    get attClass() {\n        return {\n            ...super.attClass,\n            \"o-ongoingCall\": this.thread.rtcSessions.length > 0,\n        };\n    },\n    get attClassContainer() {\n        return {\n            ...super.attClassContainer,\n            \"o-selfInCall\": this.store.rtc.selfSession?.in(this.thread.rtcSessions),\n        };\n    },\n    get bordered() {\n        return super.bordered || this.thread.rtcSessions.length > 0;\n    },\n});\n", "import { Discuss } from \"@mail/core/public_web/discuss\";\nimport { Call } from \"@mail/discuss/call/common/call\";\nimport { useState } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\nObject.assign(Discuss.components, { Call });\n\npatch(Discuss.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.rtc = useState(useService(\"discuss.rtc\"));\n    },\n});\n", "import { ChatWindow } from \"@mail/core/common/chat_window\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\nimport { useEffect } from \"@odoo/owl\";\n\npatch(ChatWindow.prototype, {\n    setup(...args) {\n        super.setup(...args);\n        useEffect(\n            () => {\n                if (this.props.chatWindow.thread === this.store.openInviteThread) {\n                    this.threadActions.actions\n                        .find((action) => action.id === \"invite-people\")\n                        ?.onSelect();\n                    this.store.openInviteThread = null;\n                }\n            },\n            () => [this.store.openInviteThread]\n        );\n    },\n});\n", "import { Discuss } from \"@mail/core/public_web/discuss\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\nimport { useEffect } from \"@odoo/owl\";\n\npatch(Discuss.prototype, {\n    setup(...args) {\n        super.setup(...args);\n        useEffect(\n            () => {\n                if (this.thread && this.thread === this.store.openInviteThread) {\n                    this.threadActions.actions\n                        .find((action) => action.id === \"invite-people\")\n                        ?.onSelect();\n                    this.store.openInviteThread = null;\n                }\n            },\n            () => [this.store.openInviteThread]\n        );\n    },\n});\n", "import { MessagingMenu } from \"@mail/core/public_web/messaging_menu\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(MessagingMenu.prototype, {\n    async onClickStartMeeting() {\n        this.store.startMeeting();\n        this.dropdown.close();\n    },\n});\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { Component, onWillStart } from \"@odoo/owl\";\nimport { useOpenChat } from \"@mail/core/web/open_chat_hook\";\n\nexport class AvatarCardPopover extends Component {\n    static template = \"mail.AvatarCardPopover\";\n\n    static props = {\n        id: { type: Number, required: true },\n        close: { type: Function, required: true },\n    };\n\n    setup() {\n        this.actionService = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.openChat = useOpenChat(\"res.users\");\n        onWillStart(async () => {\n            [this.user] = await this.orm.read(\"res.users\", [this.props.id], this.fieldNames);\n        });\n    }\n\n    get fieldNames() {\n        return [\"name\", \"email\", \"phone\", \"im_status\", \"share\", \"partner_id\"];\n    }\n\n    get email() {\n        return this.user.email;\n    }\n\n    get phone() {\n        return this.user.phone;\n    }\n\n    get showViewProfileBtn() {\n        return true;\n    }\n\n    async getProfileAction() {\n        return {\n            res_id: this.user.partner_id[0],\n            res_model: \"res.partner\",\n            type: \"ir.actions.act_window\",\n            views: [[false, \"form\"]],\n        };\n    }\n\n    get userId() {\n        return this.user.id;\n    }\n\n    onSendClick() {\n        this.openChat(this.userId);\n        this.props.close();\n    }\n\n    async onClickViewProfile() {\n        const action = await this.getProfileAction();\n        this.actionService.doAction(action);\n    }\n}\n", "import { DiscussCoreCommon } from \"@mail/discuss/core/common/discuss_core_common_service\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(DiscussCoreCommon.prototype, {\n    _handleNotificationChannelDelete(thread, metadata) {\n        const { notifId } = metadata;\n        const filteredStarredMessages = [];\n        let starredCounter = 0;\n        for (const msg of this.store.starred.messages) {\n            if (!msg.thread?.eq(thread)) {\n                filteredStarredMessages.push(msg);\n            } else {\n                starredCounter++;\n            }\n        }\n        this.store.starred.messages = filteredStarredMessages;\n        if (notifId > this.store.starred.counter_bus_id) {\n            this.store.starred.counter -= starredCounter;\n        }\n        this.store.inbox.messages = this.store.inbox.messages.filter(\n            (msg) => !msg.thread?.eq(thread)\n        );\n        if (notifId > this.store.inbox.counter_bus_id) {\n            this.store.inbox.counter -= thread.message_needaction_counter;\n        }\n        this.store.history.messages = this.store.history.messages.filter(\n            (msg) => !msg.thread?.eq(thread)\n        );\n        if (thread.eq(this.store.discuss.thread)) {\n            this.store.inbox.setAsDiscussThread();\n        }\n        super._handleNotificationChannelDelete(thread, metadata);\n    },\n});\n", "import { formatDuration } from \"@web/core/l10n/dates\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { statusBarField, StatusBarField } from \"@web/views/fields/statusbar/statusbar_field\";\n\nexport class StatusBarDurationField extends StatusBarField {\n    static template = \"mail.StatusBarDurationField\";\n\n    getAllItems() {\n        const items = super.getAllItems();\n        const durationTracking = this.props.record.data.duration_tracking || {};\n        if (Object.keys(durationTracking).length) {\n            for (const item of items) {\n                const duration = durationTracking[item.value];\n                if (duration > 0) {\n                    item.shortTimeInStage = formatDuration(duration, false);\n                    item.fullTimeInStage = formatDuration(duration, true);\n                } else {\n                    item.shortTimeInStage = 0;\n                }\n            }\n        }\n        return items;\n    }\n}\n\nexport const statusBarDurationField = {\n    ...statusBarField,\n    component: StatusBarDurationField,\n    displayName: _t(\"Status with time\"),\n    supportedTypes: [\"many2one\"],\n    fieldDependencies: [{ name: \"duration_tracking\", type: \"JSON\" }],\n};\n\nregistry.category(\"fields\").add(\"statusbar_duration\", statusBarDurationField);\n", "/** @odoo-module **/\n\nimport { FormController } from \"@web/views/form/form_controller\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * Controller to use for an onboarding step dialog, not the\n * onboarding.onboarding.step form view itself.\n */\nexport default class OnboardingStepFormController extends FormController {\n    setup() {\n        super.setup();\n        this.action = useService('action');\n        this.orm = useService('orm');\n    }\n    /**\n     * If necessary, mark the step as done and reload the main view.\n     * @override\n     */\n    async save({ closable, ...otherParams }) {\n        const saved = await super.save(otherParams);\n        if (saved) {\n            const { reloadOnFirstValidation, reloadAlways } = this.stepConfig;\n            const validationResponse = await this.orm.call(\n                'onboarding.onboarding.step',\n                'action_validate_step',\n                [this.stepName],\n            );\n            if (reloadAlways || (reloadOnFirstValidation && validationResponse === \"JUST_DONE\")) {\n                this.action.restore(this.action.currentController.jsId);\n            } else if (closable) {\n                this.action.doAction({ type: \"ir.actions.act_window_close\" });\n            }\n        }\n        return saved;\n    }\n    /**\n     * Returns the name of the onboarding step to validate after the dialog\n     * record is saved\n     *\n     * @return {string}\n     */\n    get stepName() {\n        return ''\n    }\n    /**\n     *  Returns whether to reload the page (useful if the current\n     * view needs to be updated).\n     *\n     * @returns {{reloadAlways: boolean, reloadOnFirstValidation: boolean}}\n     */\n    get stepConfig() {\n        return { reloadAlways: false, reloadOnFirstValidation: false };\n    }\n}\n", "/** @odoo-module **/\n\nimport { Component, markup, onRendered, onWillStart, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { download } from \"@web/core/network/download\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useSetupAction } from \"@web/search/action_hook\";\nimport { Layout } from \"@web/search/layout\";\nimport { SelectCreateDialog } from \"@web/views/view_dialogs/select_create_dialog\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\n\nfunction sendCustomNotification(type, message) {\n    return {\n        type: \"ir.actions.client\",\n        tag: \"display_notification\",\n        params: {\n            \"type\": type,\n            \"message\": message\n        },\n    }\n}\n\nexport class ProductPricelistReport extends Component {\n    static props = { ...standardActionServiceProps };\n    static components = { Layout };\n    static template = \"product.ProductPricelistReport\";\n\n    setup() {\n        this.action = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.dialog = useService(\"dialog\");\n\n        this.MAX_QTY = 5;\n        const pastState = this.props.state || {};\n\n        const active_model = pastState.activeModel || this.props.action.context.active_model;\n        this.noProducts = active_model === 'product.pricelist';\n        this.activeIds = this.noProducts ? [] : pastState.activeIds || this.props.action.context.active_ids;\n        this.activeModel = this.noProducts ? 'product.template' : active_model;\n        this.defaultPricelistId = this.noProducts ? this.props.action.context.active_id : false;\n\n        this.state = useState({\n            displayPricelistTitle: pastState.displayPricelistTitle || false,\n            html: \"\",\n            pricelists: [],\n            _quantities: pastState.quantities || [1, 5, 10],\n            selectedPricelist: {},\n        });\n\n        onWillStart(async () => {\n            this.state.pricelists = await this.getPricelists();\n            if (this.defaultPricelistId) {\n                this.state.selectedPricelist = this.pricelists.find(p => p.id === this.defaultPricelistId) || this.pricelists[0];\n            } else {\n                this.state.selectedPricelist = pastState.selectedPricelist || this.pricelists[0];\n            }\n            if(this.noProducts){\n                await this.onClickAddProducts();\n            }\n            this.renderHtml();\n        });\n\n        onRendered(() => {\n            this.env.config.setDisplayName(_t(\"Pricelist Report\"));\n        });\n\n        /*\n        When following the link of a product and coming back we need to keep the\n        precedent state:\n            - if the pricelist was being showed\n            - wich pricelist is selected at the moment\n            - which quantities\n        */\n        useSetupAction({\n            getLocalState: () => {\n                return {\n                    displayPricelistTitle: this.displayPricelistTitle,\n                    quantities: this.quantities,\n                    selectedPricelist: this.selectedPricelist,\n                    activeModel: this.activeModel,\n                    activeIds: this.activeIds,\n                };\n            },\n        });\n    }\n\n    // getters and setters\n\n    get displayPricelistTitle() {\n        return this.state.displayPricelistTitle;\n    }\n\n    get html() {\n        return this.state.html;\n    }\n\n    get pricelists() {\n        return this.state.pricelists;\n    }\n\n    get quantities() {\n        return this.state._quantities;\n    }\n\n    set quantities(value) {\n        this.state._quantities = value;\n    }\n\n    get reportParams() {\n        return {\n            active_model: this.activeModel || 'product.template',\n            active_ids: this.activeIds || [],\n            display_pricelist_title: this.displayPricelistTitle || '',\n            pricelist_id: this.selectedPricelist.id || '',\n            quantities: this.quantities || [1],\n        };\n    }\n\n    get selectedPricelist() {\n        return this.state.selectedPricelist;\n    }\n\n    // orm calls\n\n    getPricelists() {\n        return this.orm.searchRead(\"product.pricelist\", [], [\"id\", \"name\"]);\n    }\n\n    async renderHtml() {\n        if (this.noProducts) {\n            // do not make an rpc to get empty report data\n            this.state.html = \"\";\n            return\n        }\n        let html = await this.orm.call(\n            \"report.product.report_pricelist\", \"get_html\", [], {data: this.reportParams}\n        );\n        this.state.html = markup(html);\n    }\n\n    // events\n\n    async onClickAddQty(ev) {\n        ev.preventDefault(); // avoid automatic reloading of the page\n\n        if (this.quantities.length >= this.MAX_QTY) {\n            let message = _t(\n                \"At most %s quantities can be displayed simultaneously. Remove a selected quantity to add others.\",\n                this.MAX_QTY\n            );\n            await this.action.doAction(sendCustomNotification(\"warning\", message));\n            return;\n        }\n\n        const qty = parseInt(ev.target.previousSibling.value);\n        if (qty > 0) {\n            // Check qty already exist.\n            if (this.quantities.indexOf(qty) === -1) {\n                this.quantities.push(qty);\n                this.quantities = this.quantities.sort((a, b) => a - b);\n                this.renderHtml();\n            } else {\n                let message = _t(\"Quantity already present (%s).\", qty);\n                await this.action.doAction(sendCustomNotification(\"info\", message));\n            }\n        } else {\n            await this.action.doAction(\n                sendCustomNotification(\"info\", _t(\"Please enter a positive whole number.\"))\n            );\n        }\n    }\n\n    onClickLink(ev) {\n        ev.preventDefault();\n\n        const parent = ev.target.parentElement;\n\n        let classes = parent.getAttribute(\"class\", \"\");\n        let resModel = parent.getAttribute(\"data-model\", \"\");\n        let resId = parent.getAttribute(\"data-res-id\", \"\");\n\n        if (classes && classes.includes(\"o_action\") && resModel && resId) {\n            this.action.doAction({\n                type: 'ir.actions.act_window',\n                res_model: resModel,\n                res_id: parseInt(resId),\n                views: [[false, 'form']],\n                target: 'self',\n            });\n        }\n    }\n\n    async onClickPrint() {\n        if (this.noProducts) {\n            this.action.doAction(\n                sendCustomNotification(\"warning\", _t(\"Please select some products first.\"))\n            );\n            return;\n        }\n        const selectedFormat = document.getElementById('formats').value;\n        if (selectedFormat === 'pdf') {\n            this.export_pdf();\n        } else {\n            await this.export_pricelist_csv_xlsx(selectedFormat);\n        }\n    }\n\n    export_pdf() {\n        this.action.doAction({\n            type: 'ir.actions.report',\n            report_type: 'qweb-pdf',\n            report_name: 'product.report_pricelist',\n            report_file: 'product.report_pricelist',\n            data: this.reportParams,\n        });\n    }\n\n    async export_pricelist_csv_xlsx(format) {\n        try {\n            await download({\n                url: `/product/export/pricelist/`,\n                data: {\n                    report_data: JSON.stringify(this.reportParams),\n                    export_format: format,\n                }\n            });\n        } catch (error) {\n            console.error(`Error exporting ${format.toUpperCase()} file:`, error);\n            await this.action.doAction(\n                sendCustomNotification(\n                    \"danger\",\n                    _t(\"Error exporting file. Please try again.\")\n                )\n            );\n        }\n    }\n\n    async onClickAddProducts() {\n        this.dialog.add(SelectCreateDialog, {\n            resModel: this.activeModel || 'product.template',\n            title: _t(\"Add Products to pricelist report\"),\n            noCreate: true,\n            onSelected: async (resIds) => {\n                resIds.forEach((id) => {\n                    if (!this.activeIds.includes(id)) {\n                        this.activeIds.push(id);\n                    }\n                });\n                this.noProducts = false;\n                await this.renderHtml();\n            },\n        });\n    }\n\n    async onClickRemoveQty(ev) {\n        if (this.quantities.length <= 1) {\n            await this.action.doAction(\n                sendCustomNotification(\"warning\", _t(\"You must leave at least one quantity.\"))\n            );\n            return;\n        }\n\n        const qty = parseInt(ev.srcElement.parentElement.childNodes[0].data);\n        this.quantities = this.quantities.filter(q => q !== qty);\n        this.renderHtml();\n    }\n\n    onSelectPricelist(ev) {\n        this.state.selectedPricelist = this.pricelists.filter(pricelist =>\n            pricelist.id === parseInt(ev.target.value)\n        )[0];\n\n        this.renderHtml();\n    }\n\n    onToggleDisplayPricelist() {\n        this.state.displayPricelistTitle = !this.displayPricelistTitle;\n        this.renderHtml();\n    }\n}\n\nregistry.category(\"actions\").add(\"generate_pricelist_report\", ProductPricelistReport);\n", "/** @odoo-module */\n\nimport { _t } from '@web/core/l10n/translation';\nimport { ConfirmationDialog, deleteConfirmationMessage } from '@web/core/confirmation_dialog/confirmation_dialog';\nimport { ListRenderer } from '@web/views/list/list_renderer';\nimport { registry } from '@web/core/registry';\nimport { useService } from '@web/core/utils/hooks';\nimport { X2ManyField, x2ManyField } from '@web/views/fields/x2many/x2many_field';\n\n\nexport class PAVListRenderer extends ListRenderer {\n    setup() {\n        super.setup();\n        this.dialog = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n    }\n\n    async onDeleteRecord(record) {\n        const message = await this.orm.call(\n            'product.attribute.value',\n            'check_is_used_on_products',\n            [record.resId],\n        )\n        if (message) {\n            return this.dialog.add(ConfirmationDialog, {\n                title: _t(\"Invalid Operation\"),\n                body: message,\n            });\n        }\n        if (record.isNew) {\n            return super.onDeleteRecord(...arguments);\n        }\n        this.dialog.add(ConfirmationDialog, {\n            title: _t(\"Bye-bye, record!\"),\n            body: deleteConfirmationMessage,\n            confirmLabel: _t(\"Delete\"),\n            confirm: () => this.onConfirmDelete(record),\n            cancel: () => { },\n            cancelLabel: _t(\"No, keep it\"),\n        });\n    }\n\n    async onConfirmDelete(record) {\n        await this.orm.unlink('product.attribute.value', [record.resId])\n        const res = await super.onDeleteRecord(record);\n        await this.props.list.model.root.save();\n        return res;\n    }\n}\n\nexport class PAVOne2ManyField extends X2ManyField {\n    static components = {\n        ...X2ManyField.components,\n        ListRenderer: PAVListRenderer,\n    };\n}\n\nexport const pavOne2ManyField = {\n    ...x2ManyField,\n    component: PAVOne2ManyField,\n}\n\nregistry.category(\"fields\").add(\"pavs_one2many\", pavOne2ManyField);\n", "/** @odoo-module **/\n\nimport { UploadButton } from '@product/js/product_document_kanban/upload_button/upload_button';\nimport { KanbanController } from '@web/views/kanban/kanban_controller';\n\nexport class ProductDocumentKanbanController extends KanbanController {\n    static components = { ...KanbanController.components, UploadButton };\n\n    setup() {\n        super.setup();\n        this.uploadRoute = '/product/document/upload';\n        this.formData = {\n            'res_model': this.props.context.default_res_model,\n            'res_id': this.props.context.default_res_id,\n        };\n    }\n}\n", "/** @odoo-module **/\n\nimport { CANCEL_GLOBAL_CLICK, KanbanRecord } from \"@web/views/kanban/kanban_record\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useFileViewer } from \"@web/core/file_viewer/file_viewer_hook\";\n\nexport class ProductDocumentKanbanRecord extends KanbanRecord {\n    setup() {\n        super.setup();\n        this.store = useService(\"mail.store\");\n        this.fileViewer = useFileViewer();\n    }\n    /**\n     * @override\n     *\n     * Override to open the preview upon clicking the image, if compatible.\n     */\n    onGlobalClick(ev) {\n        if (ev.target.closest(CANCEL_GLOBAL_CLICK)) {\n            return;\n        } else if (ev.target.closest(\".o_kanban_previewer\")) {\n            const attachment = this.store.Attachment.insert({\n                id: this.props.record.data.ir_attachment_id[0],\n                filename: this.props.record.data.name,\n                name: this.props.record.data.name,\n                mimetype: this.props.record.data.mimetype,\n            });\n            this.fileViewer.open(attachment)\n            return;\n        }\n        return super.onGlobalClick(...arguments);\n    }\n}\n", "/** @odoo-module **/\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { KanbanRenderer } from \"@web/views/kanban/kanban_renderer\";\nimport { ProductDocumentKanbanRecord } from \"@product/js/product_document_kanban/product_document_kanban_record\";\nimport { FileUploadProgressContainer } from \"@web/core/file_upload/file_upload_progress_container\";\nimport { FileUploadProgressKanbanRecord } from \"@web/core/file_upload/file_upload_progress_record\";\n\nexport class ProductDocumentKanbanRenderer extends KanbanRenderer {\n    static components = {\n        ...KanbanRenderer.components,\n        FileUploadProgressContainer,\n        FileUploadProgressKanbanRecord,\n        KanbanRecord: ProductDocumentKanbanRecord,\n    };\n    static template = \"product.ProductDocumentKanbanRenderer\";\n    setup() {\n        super.setup();\n        this.fileUploadService = useService(\"file_upload\");\n    }\n}\n\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\n\nimport { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { ProductDocumentKanbanController } from \"@product/js/product_document_kanban/product_document_kanban_controller\";\nimport { ProductDocumentKanbanRenderer } from \"@product/js/product_document_kanban/product_document_kanban_renderer\";\n\nexport const productDocumentKanbanView = {\n    ...kanbanView,\n    Controller: ProductDocumentKanbanController,\n    Renderer: ProductDocumentKanbanRenderer,\n    buttonTemplate: \"product.ProductDocumentKanbanView.Buttons\",\n};\n\nregistry.category(\"views\").add(\"product_documents_kanban\", productDocumentKanbanView);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Component, useRef } from \"@odoo/owl\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\n\nexport class UploadButton extends Component {\n    static template = \"product.UploadButton\";\n    static props = {\n        formData: { type: Object, optional: true},\n        // See https://www.iana.org/assignments/media-types/media-types.xhtml\n        allowedMIMETypes: { type: String, optional: true},\n        load: Function,\n        uploadRoute: String,\n    }\n    static defaultProps = {\n        formData: {},\n    }\n\n    setup() {\n        this.uploadFileInputRef = useRef(\"uploadFileInput\");\n        this.fileUploadService = useService(\"file_upload\");\n        this.notification = useService('notification');\n        useBus(\n            this.fileUploadService.bus,\n            \"FILE_UPLOAD_LOADED\",\n            async () => {\n                await this.props.load();\n            },\n        );\n    }\n\n    async onFileInputChange(ev) {\n        const files = [...ev.target.files].filter(file => this.validFileType(file));\n        if (!files.length) {\n            return;\n        }\n        await this.fileUploadService.upload(\n            this.props.uploadRoute,\n            files,\n            {\n                buildFormData: (formData) => this.buildFormData(formData)\n            },\n        );\n        // Reset the file input's value so that the same file may be uploaded twice.\n        ev.target.value = \"\";\n    }\n\n    /**\n     * The `allowedMIMETypes` prop can restrict the file types users are guided to select. However,\n     * the `accept` attribute doesn't enforce strict validation; it only suggests file types for\n     * browsers.\n     *\n     * @param {File} file\n     * @returns Whether the upload file's type is in the whitelist (`allowedMIMETypes`).\n     */\n    validFileType(file) {\n        if (this.props.allowedMIMETypes && !this.props.allowedMIMETypes.includes(file.type)) {\n            this.notification.add(\n                _t(`Oops! '%(fileName)s' didn\u2019t upload since its format isn\u2019t allowed.`, {\n                    fileName: file.name,\n                }),\n                {\n                    type: \"danger\",\n                }\n            );\n            return false;\n        }\n        return true;\n    }\n\n    buildFormData(formData) {\n        for (const [key, value] of Object.entries(this.props.formData)) {\n            formData.append(key, value);\n        }\n    }\n\n}\n", "/** @odoo-module **/\n\nimport { KanbanController } from \"@web/views/kanban/kanban_controller\";\nimport { onWillStart } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useDebounced } from \"@web/core/utils/timing\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class ProductCatalogKanbanController extends KanbanController {\n    static template = \"ProductCatalogKanbanController\";\n\n    setup() {\n        super.setup();\n        this.action = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.orderId = this.props.context.order_id;\n        this.orderResModel = this.props.context.product_catalog_order_model;\n        this.backToQuotationDebounced = useDebounced(this.backToQuotation, 500)\n\n        onWillStart(async () => this._defineButtonContent());\n    }\n\n    // Force the slot for the \"Back to Quotation\" button to always be shown.\n    get canCreate() {\n        return true;\n    }\n\n    async _defineButtonContent() {\n        // Define the button's label depending of the order's state.\n        const orderStateInfo = await this.orm.searchRead(\n            this.orderResModel, [[\"id\", \"=\", this.orderId]], [\"state\"]\n        );\n        const orderIsQuotation = [\"draft\", \"sent\"].includes(orderStateInfo[0].state);\n        if (orderIsQuotation) {\n            this.buttonString = _t(\"Back to Quotation\");\n        } else {\n            this.buttonString = _t(\"Back to Order\");\n        }\n    }\n\n    async backToQuotation() {\n        // Restore the last form view from the breadcrumbs if breadcrumbs are available.\n        // If, for some weird reason, the user reloads the page then the breadcrumbs are\n        // lost, and we fall back to the form view ourselves.\n        if (this.env.config.breadcrumbs.length > 1) {\n            await this.action.restore();\n        } else {\n            await this.action.doAction({\n                type: \"ir.actions.act_window\",\n                res_model: this.orderResModel,\n                views: [[false, \"form\"]],\n                view_mode: \"form\",\n                res_id: this.orderId,\n            });\n        }\n    }\n}\n", "/** @odoo-module */\n\nimport { rpc } from \"@web/core/network/rpc\";\nimport { Record } from \"@web/model/relational_model/record\";\nimport { RelationalModel } from \"@web/model/relational_model/relational_model\";\n\nclass ProductCatalogRecord extends Record {\n    setup(config, data, options = {}) {\n        this.productCatalogData = data.productCatalogData;\n        data = { ...data };\n        delete data.productCatalogData;\n        super.setup(config, data, options);\n    }\n}\n\nexport class ProductCatalogKanbanModel extends RelationalModel {\n    static Record = ProductCatalogRecord;\n\n    async _loadData(params) {\n        const result = await super._loadData(...arguments);\n        if (!params.isMonoRecord && !params.groupBy.length) {\n            const orderLinesInfo = await rpc(\"/product/catalog/order_lines_info\", this._getOrderLinesInfoParams(params, result.records.map((rec) => rec.id)));\n            for (const record of result.records) {\n                record.productCatalogData = orderLinesInfo[record.id];\n            }\n        }\n        return result;\n    }\n\n    _getOrderLinesInfoParams(params, productIds) {\n        return {\n            order_id: params.context.order_id,\n            product_ids: productIds,\n            res_model: params.context.product_catalog_order_model,\n            child_field: params.context?.child_field,\n        }\n    }\n}\n", "/** @odoo-module */\nimport { useSubEnv } from \"@odoo/owl\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useDebounced } from \"@web/core/utils/timing\";\nimport { KanbanRecord } from \"@web/views/kanban/kanban_record\";\nimport { ProductCatalogOrderLine } from \"./order_line/order_line\";\n\nexport class ProductCatalogKanbanRecord extends KanbanRecord {\n    static template = \"ProductCatalogKanbanRecord\";\n    static components = {\n        ...KanbanRecord.components,\n        ProductCatalogOrderLine,\n    };\n\n    setup() {\n        super.setup();\n        this.debouncedUpdateQuantity = useDebounced(this._updateQuantity, 500, {\n            execBeforeUnmount: true,\n        });\n\n        useSubEnv({\n            currencyId: this.props.record.context.product_catalog_currency_id,\n            orderId: this.props.record.context.product_catalog_order_id,\n            orderResModel: this.props.record.context.product_catalog_order_model,\n            digits: this.props.record.context.product_catalog_digits,\n            displayUoM: this.props.record.context.display_uom,\n            precision: this.props.record.context.precision,\n            productId: this.props.record.resId,\n            addProduct: this.addProduct.bind(this),\n            removeProduct: this.removeProduct.bind(this),\n            increaseQuantity: this.increaseQuantity.bind(this),\n            setQuantity: this.setQuantity.bind(this),\n            decreaseQuantity: this.decreaseQuantity.bind(this),\n            childField: this.props.record.context?.child_field\n        });\n    }\n\n    get orderLineComponent() {\n        return ProductCatalogOrderLine;\n    }\n\n    get productCatalogData() {\n        return this.props.record.productCatalogData;\n    }\n\n    onGlobalClick(ev) {\n        // avoid a concurrent update when clicking on the buttons (that are inside the record)\n        if (ev.target.closest(\".o_product_catalog_cancel_global_click\")) {\n            return;\n        }\n        if (this.productCatalogData.quantity === 0) {\n            this.addProduct();\n        } else {\n            this.increaseQuantity();\n        }\n    }\n\n    //--------------------------------------------------------------------------\n    // Data Exchanges\n    //--------------------------------------------------------------------------\n\n    async _updateQuantity() {\n        const price = await this._updateQuantityAndGetPrice();\n        this.productCatalogData.price = parseFloat(price);\n    }\n\n    _updateQuantityAndGetPrice() {\n        return rpc(\"/product/catalog/update_order_line_info\", this._getUpdateQuantityAndGetPriceParams());\n    }\n\n    _getUpdateQuantityAndGetPriceParams() {\n        return {\n            order_id: this.env.orderId,\n            product_id: this.env.productId,\n            quantity: this.productCatalogData.quantity,\n            res_model: this.env.orderResModel,\n            child_field: this.env.childField,\n        }\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    updateQuantity(quantity) {\n        if (this.productCatalogData.readOnly) {\n            return;\n        }\n        this.productCatalogData.quantity = quantity || 0;\n        this.debouncedUpdateQuantity();\n    }\n\n    /**\n     * Add the product to the order\n     */\n    addProduct(qty=1) {\n        this.updateQuantity(qty);\n    }\n\n    /**\n     * Remove the product to the order\n     */\n    removeProduct() {\n        this.updateQuantity(0);\n    }\n\n    /**\n     * Increase the quantity of the product on the order line.\n     */\n    increaseQuantity(qty=1) {\n        this.updateQuantity(this.productCatalogData.quantity + qty);\n    }\n\n    /**\n     * Set the quantity of the product on the order line.\n     *\n     * @param {Event} event\n     */\n    setQuantity(event) {\n        this.updateQuantity(parseFloat(event.target.value));\n    }\n\n    /**\n     * Decrease the quantity of the product on the order line.\n     */\n    decreaseQuantity() {\n        this.updateQuantity(parseFloat(this.productCatalogData.quantity - 1));\n    }\n}\n", "/** @odoo-module **/\n\nimport { KanbanRenderer } from \"@web/views/kanban/kanban_renderer\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { ProductCatalogKanbanRecord } from \"./kanban_record\";\n\nexport class ProductCatalogKanbanRenderer extends KanbanRenderer {\n    static template = \"ProductCatalogKanbanRenderer\";\n    static components = {\n        ...KanbanRenderer.components,\n        KanbanRecord: ProductCatalogKanbanRecord,\n    };\n\n    setup() {\n        super.setup();\n        this.action = useService(\"action\");\n    }\n\n    get createProductContext() {\n        return {};\n    }\n\n    async createProduct() {\n        await this.action.doAction(\n            {\n                type: \"ir.actions.act_window\",\n                res_model: \"product.product\",\n                target: \"new\",\n                views: [[false, \"form\"]],\n                view_mode: \"form\",\n                context: this.createProductContext,\n            },\n            {\n                onClose: () => this.props.list.model.load(),\n            }\n        );\n    }\n}\n", "/** @odoo-module **/\n\nimport { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { registry } from \"@web/core/registry\";\n\nimport { ProductCatalogKanbanController } from \"./kanban_controller\";\nimport { ProductCatalogKanbanModel } from \"./kanban_model\";\nimport { ProductCatalogKanbanRenderer } from \"./kanban_renderer\";\nimport { ProductCatalogSearchPanel} from \"./search/search_panel\";\n\n\nexport const productCatalogKanbanView = {\n    ...kanbanView,\n    Controller: ProductCatalogKanbanController,\n    Model: ProductCatalogKanbanModel,\n    Renderer: ProductCatalogKanbanRenderer,\n    SearchPanel: ProductCatalogSearchPanel,\n};\n\nregistry.category(\"views\").add(\"product_kanban_catalog\", productCatalogKanbanView);\n", "/** @odoo-module */\nimport { Component } from \"@odoo/owl\";\nimport { formatFloat, formatMonetary } from \"@web/views/fields/formatters\";\n\nexport class ProductCatalogOrderLine extends Component {\n    static template = \"product.ProductCatalogOrderLine\";\n    static props = {\n        productId: Number,\n        quantity: Number,\n        price: Number,\n        productType: String,\n        readOnly: { type: Boolean, optional: true },\n        warning: { type: String, optional: true},\n    };\n\n    /**\n     * Focus input text when clicked\n     * @param {Event} ev \n     */\n    _onFocus(ev) {\n        ev.target.select();\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    isInOrder() {\n        return this.props.quantity !== 0;\n    }\n\n    get disableRemove() {\n        return false;\n    }\n\n    get disabledButtonTooltip() {\n        return \"\";\n    }\n\n    get price() {\n        const { currencyId, digits } = this.env;\n        return formatMonetary(this.props.price, { currencyId, digits });\n    }\n\n    get quantity() {\n        const digits = [false, this.env.precision];\n        const options = { digits, decimalPoint: \".\", thousandsSep: \"\" };\n        return parseFloat(formatFloat(this.props.quantity, options));\n    }\n\n    get showPrice() {\n        return true;\n    }\n}\n", "/** @odoo-module **/\n\nimport { SearchPanel } from \"@web/search/search_panel/search_panel\";\nimport { useState } from \"@odoo/owl\";\n\n\nexport class ProductCatalogSearchPanel extends SearchPanel {\n    static subTemplates = {\n        ...SearchPanel.subTemplates,\n        filtersGroup: \"ProductCatalogSearchPanel.FiltersGroup\",\n    };\n\n    setup() {\n        super.setup();\n\n        this.state = useState({\n            ...this.state,\n            sectionOfAttributes: {},\n        });\n    }\n\n    updateActiveValues() {\n        super.updateActiveValues();\n        this.state.sectionOfAttributes = this.buildSection();\n    }\n\n    buildSection() {\n        const values = this.env.searchModel.filters[0].values;\n        let sections = new Map();\n\n        values.forEach(element => {\n            const name = element.display_name;\n            const id = element.id;\n            const count = element.__count;\n\n            if (sections.has(name)) {\n                let currentAttr = sections.get(name);\n                currentAttr.get('ids').push(id);\n                currentAttr.set('count', currentAttr.get('count') + count);\n            } else if (count > 0) {\n                let newAttr = new Map();\n                newAttr.set('ids', [id]);\n                newAttr.set('count', count);\n                sections.set(name, newAttr);\n            }\n        });\n\n        return sections;\n    }\n\n    toggleSectionFilterValue(filterId, attrIds, { currentTarget }) {\n        attrIds.forEach(id => {\n            this.toggleFilterValue(filterId, id, { currentTarget });\n        })\n    }\n}\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { evaluateExpr } from \"@web/core/py_js/py\";\nimport { getNextTabableElement, getPreviousTabableElement } from \"@web/core/utils/ui\";\nimport { usePosition } from \"@web/core/position/position_hook\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { shallowEqual } from \"@web/core/utils/arrays\";\nimport { roundDecimals } from \"@web/core/utils/numbers\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\n\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { TagsList } from \"@web/core/tags_list/tags_list\";\nimport { useOpenMany2XRecord } from \"@web/views/fields/relational_utils\";\nimport { formatPercentage } from \"@web/views/fields/formatters\";\n\nimport { Record } from \"@web/model/record\";\nimport { Field } from \"@web/views/fields/field\";\nimport {\n    Component,\n    useState,\n    useRef,\n    useExternalListener,\n    onWillStart,\n    onPatched,\n} from \"@odoo/owl\";\n\nexport class AnalyticDistribution extends Component {\n    static template = \"analytic.AnalyticDistribution\";\n    static components = {\n        TagsList,\n        Record,\n        Field,\n    }\n\n    static props = {\n        ...standardFieldProps,\n        business_domain: { type: String, optional: true },\n        account_field: { type: String, optional: true },\n        product_field: { type: String, optional: true },\n        amount_field: { type: String, optional: true },\n        business_domain_compute: { type: String, optional: true },\n        force_applicability: { type: String, optional: true },\n        allow_save: { type: Boolean, optional: true },\n    }\n\n    setup(){\n        this.orm = useService(\"orm\");\n        this.batchedOrm = useService(\"batchedOrm\");\n\n        this.state = useState({\n            showDropdown: false,\n            formattedData: [],\n        });\n\n        this.widgetRef = useRef(\"analyticDistribution\");\n        this.dropdownRef = useRef(\"analyticDropdown\");\n        this.mainRef = useRef(\"mainElement\");\n        this.addLineButton = useRef(\"addLineButton\");\n        usePosition(\"analyticDropdown\", () => this.widgetRef.el);\n\n        this.nextId = 1;\n        this.focusSelector = false;\n\n        this.currentValue = this.props.record.data[this.props.name];\n\n        onWillStart(this.willStart);\n        useRecordObserver(this.willUpdateRecord.bind(this));\n        onPatched(this.patched);\n\n        useExternalListener(window, \"click\", this.onWindowClick, true);\n        useExternalListener(window, \"resize\", this.onWindowResized);\n\n        this.openTemplate = useOpenMany2XRecord({\n            resModel: \"account.analytic.distribution.model\",\n            activeActions: {\n                create: true,\n                edit: false,\n                write: true,\n            },\n            isToMany: false,\n            onRecordSaved: async (record) => {\n                if (!this.props.record.model.multiEdit) {\n                    this.mainRef.el.focus();\n                }\n            },\n            onClose: () => {\n                if (!this.props.record.model.multiEdit) {\n                    this.mainRef.el.focus();\n                }\n            },\n            fieldString: _t(\"Analytic Distribution Model\"),\n        });\n        this.allPlans = [];\n        this.lastAccount = this.props.account_field && this.props.record.data[this.props.account_field] || false;\n        this.lastProduct = this.props.product_field && this.props.record.data[this.props.product_field] || false;\n    }\n\n    // Lifecycle\n    async willStart() {\n        if (this.editingRecord) {\n            // for performance in list views, plans are not retrieved until they are required.\n            await this.fetchAllPlans(this.props);\n        }\n        await this.jsonToData(this.props.record.data[this.props.name]);\n    }\n\n    async willUpdateRecord(record) {\n        // Unless force_applicability, Plans need to be retrieved again as the product or account might have changed\n        // and thus different applicabilities apply\n        // or a model applies that contains unavailable plans\n        // This should only execute when these fields have changed, therefore we use the `_field` props.\n        const valueChanged =\n            JSON.stringify(this.currentValue) !==\n            JSON.stringify(record.data[this.props.name]);\n        const currentAccount = this.props.account_field && record.data[this.props.account_field] || false;\n        const currentProduct = this.props.product_field && record.data[this.props.product_field] || false;\n        const accountChanged = !shallowEqual(this.lastAccount, currentAccount);\n        const productChanged = !shallowEqual(this.lastProduct, currentProduct);\n        if (valueChanged || accountChanged || productChanged) {\n            if (!this.props.force_applicability) {\n                await this.fetchAllPlans({ record });\n            }\n            this.lastAccount = accountChanged && currentAccount || this.lastAccount;\n            this.lastProduct = productChanged && currentProduct || this.lastProduct;\n            await this.jsonToData(record.data[this.props.name]);\n        }\n        this.currentValue = record.data[this.props.name];\n    }\n\n    patched() {\n        this.focusToSelector();\n    }\n\n    /**\n     * Computes the totals for each account, grouped by plan (primarily used in tags)\n     * @returns {Object}\n     */\n    accountTotalsByPlan() {\n        const accountTotals = {};\n        this.state.formattedData.map((line) => {\n            line.analyticAccounts.map((column) => {\n                if (column.accountId) {\n                    let {\n                        accId = column.accountId,\n                        accName = column.accountDisplayName,\n                        total = 0.0,\n                        planId = column.accountRootPlanId,\n                        planColor = column.accountColor,\n                    } = accountTotals[column.accountRootPlanId]?.[column.accountId] || {};\n\n                    total += roundDecimals(line.percentage, this.decimalPrecision.digits[1] + 2);\n\n                    accountTotals[planId] = accountTotals[planId] || {};\n                    accountTotals[planId][accId] = { accId, accName, planId, total, planColor};\n                }\n            })\n        });\n        return accountTotals;\n    }\n\n    /**\n     * Computes the totals for each plan (used in the table headers)\n     * @returns {Object}\n     */\n    planTotals() {\n        const summary = this.accountTotalsByPlan();\n        this.allPlans.map((plan) => {\n            const planTotal = (summary[plan.id] && Object.values(summary[plan.id]) || []).reduce((prev, next) => prev + next.total, 0.0);\n            const className = plan.applicability === \"mandatory\" && !this.planIsComplete(planTotal) ? 'text-danger' : plan.applicability === \"mandatory\" ? 'text-success' : '';\n            summary[plan.id] = {\n                value: planTotal,\n                formattedValue: formatPercentage(planTotal, this.decimalPrecision),\n                class: className,\n                applicability: plan.applicability,\n            }\n        });\n        return summary;\n    }\n\n    planIsComplete(total) {\n        return roundDecimals(total, this.decimalPrecision.digits[1] + 2) === 1;\n    }\n\n    /**\n     * Converts the account Totals to a list of tags\n     * PlanA  PlanB  PlanC  Percentage\n     * A1                   100\n     *        B1            80.123     => [\"A1\", \"80.12% B1\", \"C1\"]\n     *               C1     100\n     *\n     * PlanA  PlanB  PlanC  Percentage\n     * A1     B1     C1     50\n     * A2     B1     C1     50         => [\"50% A1 | 50% A2 | 50% A3\", \"150% B1\", \"C1 | 50% C2\"]\n     * A3     B1     C2     50\n     * @returns [List] of tag objects\n     */\n    planSummaryTags() {\n        const accountTotals = this.accountTotalsByPlan();\n        return Object.values(accountTotals).map((planSummary) => {\n            const accs = Object.values(planSummary);\n            return {\n                id: accs[0].planId,\n                text: accs.reduce((p, n) => p + (p.length ? \" | \" : \"\") + (this.planIsComplete(n.total) ? n.accName : `${formatPercentage(n.total)} ${n.accName}`) , \"\"),\n                colorIndex: accs[0].planColor,\n                onClick: (ev) => this.tagClicked(ev),\n            };\n        });\n    }\n\n    plansToArray() {\n        return this.allPlans.map((plan) => ({\n            planId: plan.id,\n            planName: plan.name,\n            planColor: plan.color,\n        }));\n    }\n\n    async jsonToData(jsonFieldValue) {\n        const analyticAccountIds = jsonFieldValue ? Object.keys(jsonFieldValue).map((key) => key.split(',')).flat().map((id) => parseInt(id)) : [];\n        const analyticAccountDict = analyticAccountIds.length ? await this.fetchAnalyticAccounts([[\"id\", \"in\", analyticAccountIds]]) : [];\n\n        let distribution = [];\n        let accountNotFound = false;\n\n        for (const [accountIds, percentage] of Object.entries(jsonFieldValue)) {\n            const defaultVals = this.plansToArray(); // empty if the popup was not opened\n            const ids = accountIds.split(',');\n\n            for (const id of ids) {\n                const account = analyticAccountDict[parseInt(id)];\n                if (account) {\n                    // since tags are displayed even though plans might not be retrieved (ie defaultVals is empty)\n                    // push the accounts anyway, as order doesn't matter\n                    // once the popup is opened, plans are fetched and the analyticAccounts list will be ordered\n                    Object.assign(defaultVals.find((plan) => plan.planId == account.root_plan_id[0]) || defaultVals.push({}) && defaultVals[defaultVals.length-1],\n                    {\n                        accountId: parseInt(id),\n                        accountDisplayName: account.display_name,\n                        accountColor: account.color,\n                        accountRootPlanId: account.root_plan_id[0],\n                    });\n                } else {\n                    accountNotFound = true;\n                }\n            }\n            distribution.push({\n                analyticAccounts: defaultVals,\n                percentage: percentage / 100,\n                id: this.nextId++,\n            })\n        }\n        this.state.formattedData = distribution;\n        if (accountNotFound) {\n            // Analytic accounts in the json were not found, save the json without them\n            await this.save();\n        }\n    }\n\n    recordProps(line) {\n        const analyticAccountFields = {\n            id: { type: \"int\" },\n            display_name: { type: \"char\" },\n            color: { type: \"int\" },\n            plan_id: { type: \"many2one\" },\n            root_plan_id: { type: \"many2one\" },\n        };\n        let recordFields = {};\n        const values = {};\n        // Analytic Account fields\n        line.analyticAccounts.map((account) => {\n            const fieldName = `x_plan${account.planId}_id`;\n            recordFields[fieldName] = {\n                string: account.planName,\n                relation: \"account.analytic.account\",\n                type: \"many2one\",\n                related: {\n                    fields: analyticAccountFields,\n                    activeFields: analyticAccountFields,\n                },\n                // company domain might be required here\n                domain: [[\"root_plan_id\", \"=\", account.planId]],\n            };\n            values[fieldName] =  account?.accountId || false;\n        });\n        // Percentage field\n        recordFields['percentage'] = {\n            string: _t(\"Percentage\"),\n            type: \"percentage\",\n            cellClass: \"numeric_column_width\",\n            ...this.decimalPrecision,\n        };\n        values['percentage'] = line.percentage;\n        // Value field copied from original\n        if (this.props.amount_field) {\n            const { string, name, type, currency_field } = this.props.record.fields[this.props.amount_field];\n            recordFields[name] = { string, name, type, currency_field, cellClass: \"numeric_column_width\" };\n            values[name] = this.props.record.data[name] * values['percentage'];\n            // Currency field\n            if (currency_field) {\n                // TODO: check web_read network request\n                const { string, name, type, relation } = this.props.record.fields[currency_field];\n                recordFields[currency_field] = { name, string, type, relation, invisible: true };\n                values[currency_field] = this.props.record.data[currency_field][0];\n            }\n        }\n        return {\n            fields: recordFields,\n            values: values,\n            activeFields: recordFields,\n            onRecordChanged: async (record, changes) => await this.lineChanged(record, changes, line),\n        }\n    }\n\n    accountCount(line) {\n        return line.analyticAccounts.map((acc) => acc.accountId).filter(Boolean).length;\n    }\n\n    lineIsValid(line) {\n        return this.accountCount(line) && line.percentage;\n    }\n\n    // ORM\n    fetchPlansArgs({ record }) {\n        let args = {};\n        if (this.props.business_domain_compute) {\n            args['business_domain'] = evaluateExpr(this.props.business_domain_compute, record.evalContext);\n        }\n        if (this.props.business_domain) {\n            args['business_domain'] = this.props.business_domain;\n        }\n        if (this.props.product_field && record.data[this.props.product_field]) {\n            args['product'] = record.data[this.props.product_field][0];\n        }\n        if (this.props.account_field && record.data[this.props.account_field]) {\n            args['account'] = record.data[this.props.account_field][0];\n        }\n        if (this.props.force_applicability) {\n            args['applicability'] = this.props.force_applicability;\n        }\n        const existing_account_ids = Object.keys(record.data[this.props.name]).map((k) => k.split(\",\")).flat().map((i) => parseInt(i));\n        if (existing_account_ids.length) {\n            args['existing_account_ids'] = existing_account_ids;\n        }\n        if (record.data.company_id) {\n            args['company_id'] = record.data.company_id[0];\n        }\n        return args;\n    }\n\n    async fetchAllPlans(props) {\n        const argsPlan = this.fetchPlansArgs(props);\n        this.allPlans = await this.orm.call(\"account.analytic.plan\", \"get_relevant_plans\", [], argsPlan);\n    }\n\n    async fetchAnalyticAccounts(domain) {\n        const args = {\n            domain: domain,\n            fields: [\"id\", \"display_name\", \"root_plan_id\", \"color\"],\n            context: [],\n        }\n        // batched call\n        const records = await this.batchedOrm.read(\"account.analytic.account\", domain[0][2], args.fields, {});\n        return Object.assign({}, ...records.map((r) => {\n            const {id, ...rest} = r;\n            return {[id]: rest};\n        }));\n    }\n\n    // Editing Distributions\n    async lineChanged(record, changes, line) {\n        // record analytic account changes to the state\n        for (const account of line.analyticAccounts) {\n            const selected = record.data[`x_plan${account.planId}_id`];\n            account.accountId = selected[0];\n            account.accountDisplayName = selected[1];\n            account.accountColor = account.planColor;\n            account.accountRootPlanId = account.planId;\n        }\n        // record percentage or value changes\n        if (changes.percentage != line.percentage) {\n            roundDecimals(line.percentage = record.data.percentage, this.decimalPrecision.digits[1] + 2);\n        } else if (\n            this.valueColumnEnabled &&\n            changes[this.props.amount_field] != line[this.props.amount_field]\n        ) {\n            line.percentage = roundDecimals(\n                record.data[this.props.amount_field] / this.props.record.data[this.props.amount_field],\n                this.decimalPrecision.digits[1] + 2);\n        }\n    }\n\n    // Getters\n    get valueColumnEnabled() {\n        return Boolean(this.props.amount_field && this.props.record.data[this.props.amount_field]);\n    }\n\n    get decimalPrecision() {\n        return { digits: [12, this.props.record.data.analytic_precision || 2] };\n    }\n\n    get allowSave() {\n        return this.props.allow_save && this.state.formattedData.some((line) => this.lineIsValid(line));\n    }\n\n    get editingRecord() {\n        return !this.props.readonly;\n    }\n\n    get isDropdownOpen() {\n        return this.state.showDropdown && !!this.dropdownRef.el;\n    }\n\n    // actions\n    addLine() {\n        let maxMandatory = 0, maxOptional = 0, hasMandatory = false;\n\n        Object.values(this.planTotals()).filter((plan) => plan.value < 1).map((plan) => {\n            if (plan.applicability == \"mandatory\"){\n                maxMandatory = Math.max(plan.value, maxMandatory);\n                hasMandatory = true;\n            } else {\n                maxOptional = Math.max(plan.value, maxOptional);\n            }\n        });\n        let noPlanTotal = this.state.formattedData.filter((line) => !this.accountCount(line)).reduce((p, n) => p + n.percentage, 0);\n        const remainder = roundDecimals(1 - (hasMandatory ? maxMandatory : (maxOptional || noPlanTotal)), this.decimalPrecision.digits[1] + 2);\n        const lineToAdd = {\n            id: this.nextId++,\n            analyticAccounts: this.plansToArray(),\n            percentage: Math.max(remainder, 0) || 1,\n        }\n        this.state.formattedData.push(lineToAdd);\n        this.setFocusSelector(`[name=line_${this.state.formattedData.length - 1}] td:first-of-type`);\n    }\n\n    deleteLine(index) {\n        this.state.formattedData.splice(index, 1);\n        if (!this.state.formattedData.length) {\n            this.addLine();\n        }\n    }\n\n    dataToJson() {\n        const result = {};\n        this.state.formattedData = this.state.formattedData.filter((line) => this.accountCount(line));\n        this.state.formattedData.map((line) => {\n            const key = line.analyticAccounts.reduce((p, n) => p.concat(n.accountId ? n.accountId : []), []);\n            result[key] = (result[key] || 0) + line.percentage * 100;\n        });\n        return result;\n    }\n\n    async save() {\n        await this.props.record.update({ [this.props.name]: this.dataToJson() });\n    }\n\n    onSaveNew() {\n        this.closeAnalyticEditor();\n        const { record, product_field, account_field } = this.props;\n        this.openTemplate({ resId: false, context: {\n            'default_analytic_distribution': this.dataToJson(),\n            'default_partner_id': record.data['partner_id'] ? record.data['partner_id'][0] : undefined,\n            'default_product_id': product_field ? record.data[product_field][0] : undefined,\n            'default_account_prefix': account_field ? record.data[account_field][1].substr(0, 3) : undefined,\n        }});\n    }\n\n    forceCloseEditor() {\n        // focus to the main Element but the dropdown should not open\n        this.preventOpen = true;\n        this.closeAnalyticEditor();\n        this.mainRef.el.focus();\n        this.preventOpen = false;\n    }\n\n    closeAnalyticEditor() {\n        this.save();\n        this.state.showDropdown = false;\n    }\n\n    async openAnalyticEditor() {\n        if (!this.allPlans.length) {\n            await this.fetchAllPlans(this.props);\n            await this.jsonToData(this.props.record.data[this.props.name]);\n        }\n        if (!this.state.formattedData.length) {\n            await this.addLine();\n        }\n        this.setFocusSelector(\"[name='line_0'] td:first-of-type\");\n        this.state.showDropdown = true;\n    }\n\n    async tagClicked(ev) {\n        if (this.editingRecord && !this.isDropdownOpen) {\n            // TODO: focus is not working when tag is clicked while on an editable line\n            await this.openAnalyticEditor();\n        }\n        if (this.isDropdownOpen) {\n            this.setFocusSelector(\"[name='line_0'] td:first-of-type\");\n            this.focusToSelector();\n            ev.stopPropagation();\n        }\n    }\n\n    // Focus\n    onMainElementFocus(ev) {\n        if (!this.isDropdownOpen && !this.preventOpen) {\n            this.openAnalyticEditor();\n        }\n    }\n\n    focusToSelector() {\n        if (this.focusSelector && this.isDropdownOpen) {\n            this.focus(this.adjacentElementToFocus(\"next\", this.dropdownRef.el.querySelector(this.focusSelector)));\n        }\n        this.focusSelector = false;\n    }\n\n    setFocusSelector(selector) {\n        this.focusSelector = selector;\n    }\n\n    adjacentElementToFocus(direction, el = null) {\n        if (!this.isDropdownOpen) {\n            return null;\n        }\n        if (!el) {\n            el = this.dropdownRef.el;\n        }\n        return direction == \"next\" ? getNextTabableElement(el) : getPreviousTabableElement(el);\n    }\n\n    focusAdjacent(direction) {\n        const elementToFocus = this.adjacentElementToFocus(direction);\n        if (elementToFocus){\n            this.focus(elementToFocus);\n            return true;\n        }\n        return false;\n    }\n\n    focus(el) {\n        if (!el) return;\n        el.focus();\n        if ([\"INPUT\", \"TEXTAREA\"].includes(el.tagName)) {\n            if (el.selectionStart) {\n                el.selectionStart = 0;\n                el.selectionEnd = el.value.length;\n            }\n            el.select();\n        }\n    }\n\n    // Keys and Clicks\n    async onWidgetKeydown(ev) {\n        if (!this.editingRecord) {\n            return;\n        }\n        const hotkey = getActiveHotkey(ev);\n        switch (hotkey) {\n            case \"enter\":\n            case \"tab\": {\n                if (this.isDropdownOpen) {\n                    const closestCell = ev.target.closest(\"td, th\");\n                    const row = closestCell.parentElement;\n                    const line = this.state.formattedData[parseInt(row.id)];\n                    if (this.adjacentElementToFocus(\"next\") == this.addLineButton.el && line && this.lineIsValid(line)) {\n                        this.addLine();\n                        break;\n                    }\n                    this.focusAdjacent(\"next\") || this.forceCloseEditor();\n                    break;\n                };\n                return;\n            }\n            case \"shift+tab\": {\n                if (this.isDropdownOpen) {\n                    this.focusAdjacent(\"previous\") || this.forceCloseEditor();\n                    break;\n                };\n                return;\n            }\n            case \"escape\": {\n                if (this.isDropdownOpen) {\n                    this.forceCloseEditor();\n                    break;\n                }\n            }\n            case \"arrowdown\": {\n                if (!this.isDropdownOpen) {\n                    this.onMainElementFocus();\n                    break;\n                }\n                return;\n            }\n            default: {\n                return;\n            }\n        }\n        ev.preventDefault();\n        ev.stopPropagation();\n    }\n\n    onWindowClick(ev) {\n        /*\n        Dropdown should be closed only if all these condition are true:\n            - dropdown is open\n            - click is outside widget element (widgetRef)\n            - there is no active modal containing a list/kanban view (search more modal)\n            - there is no popover (click is not in search modal's search bar menu)\n            - click is not targeting document dom element (drag and drop search more modal)\n        */\n\n        const selectors = [\n            \".o_popover\",\n            \".modal:not(.o_inactive_modal):not(:has(.o_act_window))\",\n        ];\n        if (this.isDropdownOpen\n            && !this.widgetRef.el.contains(ev.target)\n            && !ev.target.closest(selectors.join(\",\"))\n            && !ev.target.isSameNode(document.documentElement)\n           ) {\n            this.forceCloseEditor();\n        }\n    }\n\n    onWindowResized() {\n        // popup ui is ugly when window is resized, so close it\n        if (this.isDropdownOpen && !isMobileOS()) {\n            this.forceCloseEditor();\n        }\n    }\n}\n\nexport const analyticDistribution = {\n    component: AnalyticDistribution,\n    supportedTypes: [\"char\", \"text\"],\n    fieldDependencies: [{ name:\"analytic_precision\", type: \"integer\" }],\n    supportedOptions: [\n        {\n            label: _t(\"Disable save\"),\n            name: \"disable_save\",\n            type: \"boolean\",\n        },\n        {\n            label: _t(\"Force applicability\"),\n            name: \"force_applicability\",\n            type: \"boolean\",\n        },\n        {\n            label: _t(\"Business domain\"),\n            name: \"business_domain\",\n            type: \"string\",\n        },\n        {\n            label: _t(\"Product field\"),\n            name: \"product_field\",\n            type: \"field\",\n            availableTypes: [\"many2one\"],\n        },\n        {\n            label: _t(\"Amount field\"),\n            name: \"amount_field\",\n            type: \"field\",\n            availableTypes: [\"monetary\"],\n        },\n        {\n            label: _t(\"Account field\"),\n            name: \"account_field\",\n            type: \"field\",\n            availableTypes: [\"many2one\"],\n        }\n    ],\n    extractProps: ({ attrs, options }) => ({\n        business_domain: options.business_domain,\n        account_field: options.account_field,\n        product_field: options.product_field,\n        amount_field: options.amount_field,\n        business_domain_compute: attrs.business_domain_compute,\n        force_applicability: options.force_applicability,\n        allow_save: !options.disable_save,\n    }),\n};\n\nregistry.category(\"fields\").add(\"analytic_distribution\", analyticDistribution);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { ORM } from \"@web/core/orm_service\";\nimport { unique } from \"@web/core/utils/arrays\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\n\nclass RequestBatcherORM extends ORM {\n    constructor() {\n        super();\n        this.searchReadBatches = {};\n        this.searchReadBatchId = 1;\n        this.batches = {};\n    }\n\n    /**\n     * @param {number[]} ids\n     * @param {any[]} keys\n     * @param {Function} callback\n     * @returns {Promise<any>}\n     */\n    async batch(ids, keys, callback) {\n        const key = JSON.stringify(keys);\n        let batch = this.batches[key];\n        if (!batch) {\n            batch = {\n                deferred: new Deferred(),\n                scheduled: false,\n                ids: [],\n            };\n            this.batches[key] = batch;\n        }\n        batch.ids = unique([...batch.ids, ...ids]);\n\n        if (!batch.scheduled) {\n            batch.scheduled = true;\n            Promise.resolve().then(async () => {\n                delete this.batches[key];\n                let result;\n                try {\n                    result = await callback(batch.ids);\n                } catch (e) {\n                    return batch.deferred.reject(e);\n                }\n                batch.deferred.resolve(result);\n            });\n        }\n\n        return batch.deferred;\n    }\n\n    /**\n     * Entry point to batch \"read\" calls. If the `fields` and `resModel`\n     * arguments have already been called, the given ids are added to the\n     * previous list of ids to perform a single read call. Once the server\n     * responds, records are then dispatched to the callees based on the\n     * given ids arguments (kept in the closure).\n     *\n     * @param {string} resModel\n     * @param {number[]} resIds\n     * @param {string[]} fields\n     * @returns {Promise<Object[]>}\n     */\n    async read(resModel, resIds, fields, kwargs) {\n        const records = await this.batch(resIds, [\"read\", resModel, fields, kwargs], (resIds) =>\n            super.read(resModel, resIds, fields, kwargs)\n        );\n        return records.filter((r) => resIds.includes(r.id));\n    }\n}\n\nexport const batchedOrmService = {\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 RequestBatcherORM();\n    },\n};\n\nregistry.category(\"services\").add(\"batchedOrm\", batchedOrmService);\n", "/**\r\n* vkBeautify - javascript plugin to pretty-print or minify text in XML, JSON, CSS and SQL formats.\r\n*  \r\n* Version - 0.99.00.beta \r\n* Copyright (c) 2012 Vadim Kiryukhin\r\n* vkiryukhin @ gmail.com\r\n* http://www.eslinstructor.net/vkbeautify/\r\n* \r\n* Dual licensed under the MIT and GPL licenses:\r\n*   http://www.opensource.org/licenses/mit-license.php\r\n*   http://www.gnu.org/licenses/gpl.html\r\n*\r\n*   Pretty print\r\n*\r\n*        vkbeautify.xml(text [,indent_pattern]);\r\n*        vkbeautify.json(text [,indent_pattern]);\r\n*        vkbeautify.css(text [,indent_pattern]);\r\n*        vkbeautify.sql(text [,indent_pattern]);\r\n*\r\n*        @text - String; text to beatufy;\r\n*        @indent_pattern - Integer | String;\r\n*                Integer:  number of white spaces;\r\n*                String:   character string to visualize indentation ( can also be a set of white spaces )\r\n*   Minify\r\n*\r\n*        vkbeautify.xmlmin(text [,preserve_comments]);\r\n*        vkbeautify.jsonmin(text);\r\n*        vkbeautify.cssmin(text [,preserve_comments]);\r\n*        vkbeautify.sqlmin(text);\r\n*\r\n*        @text - String; text to minify;\r\n*        @preserve_comments - Bool; [optional];\r\n*                Set this flag to true to prevent removing comments from @text ( minxml and mincss functions only. )\r\n*\r\n*   Examples:\r\n*        vkbeautify.xml(text); // pretty print XML\r\n*        vkbeautify.json(text, 4 ); // pretty print JSON\r\n*        vkbeautify.css(text, '. . . .'); // pretty print CSS\r\n*        vkbeautify.sql(text, '----'); // pretty print SQL\r\n*\r\n*        vkbeautify.xmlmin(text, true);// minify XML, preserve comments\r\n*        vkbeautify.jsonmin(text);// minify JSON\r\n*        vkbeautify.cssmin(text);// minify CSS, remove comments ( default )\r\n*        vkbeautify.sqlmin(text);// minify SQL\r\n*\r\n*/\r\n\r\n(function() {\r\n\r\nfunction createShiftArr(step) {\r\n\r\n\tvar space = '    ';\r\n\t\r\n\tif ( isNaN(parseInt(step)) ) {  // argument is string\r\n\t\tspace = step;\r\n\t} else { // argument is integer\r\n\t\tswitch(step) {\r\n\t\t\tcase 1: space = ' '; break;\r\n\t\t\tcase 2: space = '  '; break;\r\n\t\t\tcase 3: space = '   '; break;\r\n\t\t\tcase 4: space = '    '; break;\r\n\t\t\tcase 5: space = '     '; break;\r\n\t\t\tcase 6: space = '      '; break;\r\n\t\t\tcase 7: space = '       '; break;\r\n\t\t\tcase 8: space = '        '; break;\r\n\t\t\tcase 9: space = '         '; break;\r\n\t\t\tcase 10: space = '          '; break;\r\n\t\t\tcase 11: space = '           '; break;\r\n\t\t\tcase 12: space = '            '; break;\r\n\t\t}\r\n\t}\r\n\r\n\tvar shift = ['\\n']; // array of shifts\r\n\tfor(ix=0;ix<100;ix++){\r\n\t\tshift.push(shift[ix]+space); \r\n\t}\r\n\treturn shift;\r\n}\r\n\r\nfunction vkbeautify(){\r\n\tthis.step = '    '; // 4 spaces\r\n\tthis.shift = createShiftArr(this.step);\r\n};\r\n\r\nvkbeautify.prototype.xml = function(text,step) {\r\n\r\n\tvar ar = text.replace(/>\\s{0,}</g,\"><\")\r\n\t\t\t\t .replace(/</g,\"~::~<\")\r\n\t\t\t\t .replace(/\\s*xmlns\\:/g,\"~::~xmlns:\")\r\n\t\t\t\t .replace(/\\s*xmlns\\=/g,\"~::~xmlns=\")\r\n\t\t\t\t .split('~::~'),\r\n\t\tlen = ar.length,\r\n\t\tinComment = false,\r\n\t\tdeep = 0,\r\n\t\tstr = '',\r\n\t\tix = 0,\r\n\t\tshift = step ? createShiftArr(step) : this.shift;\r\n\r\n\t\tfor(ix=0;ix<len;ix++) {\r\n\t\t\t// start comment or <![CDATA[...]]> or <!DOCTYPE //\r\n\t\t\tif(ar[ix].search(/<!/) > -1) { \r\n\t\t\t\tstr += shift[deep]+ar[ix];\r\n\t\t\t\tinComment = true; \r\n\t\t\t\t// end comment  or <![CDATA[...]]> //\r\n\t\t\t\tif(ar[ix].search(/-->/) > -1 || ar[ix].search(/\\]>/) > -1 || ar[ix].search(/!DOCTYPE/) > -1 ) { \r\n\t\t\t\t\tinComment = false; \r\n\t\t\t\t}\r\n\t\t\t} else \r\n\t\t\t// end comment  or <![CDATA[...]]> //\r\n\t\t\tif(ar[ix].search(/-->/) > -1 || ar[ix].search(/\\]>/) > -1) { \r\n\t\t\t\tstr += ar[ix];\r\n\t\t\t\tinComment = false; \r\n\t\t\t} else \r\n\t\t\t// <elm></elm> //\r\n\t\t\tif( /^<\\w/.exec(ar[ix-1]) && /^<\\/\\w/.exec(ar[ix]) &&\r\n\t\t\t\t/^<[\\w:\\-\\.\\,]+/.exec(ar[ix-1]) == /^<\\/[\\w:\\-\\.\\,]+/.exec(ar[ix])[0].replace('/','')) { \r\n\t\t\t\tstr += ar[ix];\r\n\t\t\t\tif(!inComment) deep--;\r\n\t\t\t} else\r\n\t\t\t // <elm> //\r\n\t\t\tif(ar[ix].search(/<\\w/) > -1 && ar[ix].search(/<\\//) == -1 && ar[ix].search(/\\/>/) == -1 ) {\r\n\t\t\t\tstr = !inComment ? str += shift[deep++]+ar[ix] : str += ar[ix];\r\n\t\t\t} else \r\n\t\t\t // <elm>...</elm> //\r\n\t\t\tif(ar[ix].search(/<\\w/) > -1 && ar[ix].search(/<\\//) > -1) {\r\n\t\t\t\tstr = !inComment ? str += shift[deep]+ar[ix] : str += ar[ix];\r\n\t\t\t} else \r\n\t\t\t// </elm> //\r\n\t\t\tif(ar[ix].search(/<\\//) > -1) { \r\n\t\t\t\tstr = !inComment ? str += shift[--deep]+ar[ix] : str += ar[ix];\r\n\t\t\t} else \r\n\t\t\t// <elm/> //\r\n\t\t\tif(ar[ix].search(/\\/>/) > -1 ) { \r\n\t\t\t\tstr = !inComment ? str += shift[deep]+ar[ix] : str += ar[ix];\r\n\t\t\t} else \r\n\t\t\t// <? xml ... ?> //\r\n\t\t\tif(ar[ix].search(/<\\?/) > -1) { \r\n\t\t\t\tstr += shift[deep]+ar[ix];\r\n\t\t\t} else \r\n\t\t\t// xmlns //\r\n\t\t\tif( ar[ix].search(/xmlns\\:/) > -1  || ar[ix].search(/xmlns\\=/) > -1) { \r\n\t\t\t\tstr += shift[deep]+ar[ix];\r\n\t\t\t} \r\n\t\t\t\r\n\t\t\telse {\r\n\t\t\t\tstr += ar[ix];\r\n\t\t\t}\r\n\t\t}\r\n\t\t\r\n\treturn  (str[0] == '\\n') ? str.slice(1) : str;\r\n}\r\n\r\nvkbeautify.prototype.json = function(text,step) {\r\n\r\n\tvar step = step ? step : this.step;\r\n\t\r\n\tif (typeof JSON === 'undefined' ) return text; \r\n\t\r\n\tif ( typeof text === \"string\" ) return JSON.stringify(JSON.parse(text), null, step);\r\n\tif ( typeof text === \"object\" ) return JSON.stringify(text, null, step);\r\n\t\t\r\n\treturn text; // text is not string nor object\r\n}\r\n\r\nvkbeautify.prototype.css = function(text, step) {\r\n\r\n\tvar ar = text.replace(/\\s{1,}/g,' ')\r\n\t\t\t\t.replace(/\\{/g,\"{~::~\")\r\n\t\t\t\t.replace(/\\}/g,\"~::~}~::~\")\r\n\t\t\t\t.replace(/\\;/g,\";~::~\")\r\n\t\t\t\t.replace(/\\/\\*/g,\"~::~/*\")\r\n\t\t\t\t.replace(/\\*\\//g,\"*/~::~\")\r\n\t\t\t\t.replace(/~::~\\s{0,}~::~/g,\"~::~\")\r\n\t\t\t\t.split('~::~'),\r\n\t\tlen = ar.length,\r\n\t\tdeep = 0,\r\n\t\tstr = '',\r\n\t\tix = 0,\r\n\t\tshift = step ? createShiftArr(step) : this.shift;\r\n\t\t\r\n\t\tfor(ix=0;ix<len;ix++) {\r\n\r\n\t\t\tif( /\\{/.exec(ar[ix]))  { \r\n\t\t\t\tstr += shift[deep++]+ar[ix];\r\n\t\t\t} else \r\n\t\t\tif( /\\}/.exec(ar[ix]))  { \r\n\t\t\t\tstr += shift[--deep]+ar[ix];\r\n\t\t\t} else\r\n\t\t\tif( /\\*\\\\/.exec(ar[ix]))  { \r\n\t\t\t\tstr += shift[deep]+ar[ix];\r\n\t\t\t}\r\n\t\t\telse {\r\n\t\t\t\tstr += shift[deep]+ar[ix];\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn str.replace(/^\\n{1,}/,'');\r\n}\r\n\r\n//----------------------------------------------------------------------------\r\n\r\nfunction isSubquery(str, parenthesisLevel) {\r\n\treturn  parenthesisLevel - (str.replace(/\\(/g,'').length - str.replace(/\\)/g,'').length )\r\n}\r\n\r\nfunction split_sql(str, tab) {\r\n\r\n\treturn str.replace(/\\s{1,}/g,\" \")\r\n\r\n\t\t\t\t.replace(/ AND /ig,\"~::~\"+tab+tab+\"AND \")\r\n\t\t\t\t.replace(/ BETWEEN /ig,\"~::~\"+tab+\"BETWEEN \")\r\n\t\t\t\t.replace(/ CASE /ig,\"~::~\"+tab+\"CASE \")\r\n\t\t\t\t.replace(/ ELSE /ig,\"~::~\"+tab+\"ELSE \")\r\n\t\t\t\t.replace(/ END /ig,\"~::~\"+tab+\"END \")\r\n\t\t\t\t.replace(/ FROM /ig,\"~::~FROM \")\r\n\t\t\t\t.replace(/ GROUP\\s{1,}BY/ig,\"~::~GROUP BY \")\r\n\t\t\t\t.replace(/ HAVING /ig,\"~::~HAVING \")\r\n\t\t\t\t//.replace(/ SET /ig,\" SET~::~\")\r\n\t\t\t\t.replace(/ IN /ig,\" IN \")\r\n\t\t\t\t\r\n\t\t\t\t.replace(/ JOIN /ig,\"~::~JOIN \")\r\n\t\t\t\t.replace(/ CROSS~::~{1,}JOIN /ig,\"~::~CROSS JOIN \")\r\n\t\t\t\t.replace(/ INNER~::~{1,}JOIN /ig,\"~::~INNER JOIN \")\r\n\t\t\t\t.replace(/ LEFT~::~{1,}JOIN /ig,\"~::~LEFT JOIN \")\r\n\t\t\t\t.replace(/ RIGHT~::~{1,}JOIN /ig,\"~::~RIGHT JOIN \")\r\n\t\t\t\t\r\n\t\t\t\t.replace(/ ON /ig,\"~::~\"+tab+\"ON \")\r\n\t\t\t\t.replace(/ OR /ig,\"~::~\"+tab+tab+\"OR \")\r\n\t\t\t\t.replace(/ ORDER\\s{1,}BY/ig,\"~::~ORDER BY \")\r\n\t\t\t\t.replace(/ OVER /ig,\"~::~\"+tab+\"OVER \")\r\n\r\n\t\t\t\t.replace(/\\(\\s{0,}SELECT /ig,\"~::~(SELECT \")\r\n\t\t\t\t.replace(/\\)\\s{0,}SELECT /ig,\")~::~SELECT \")\r\n\t\t\t\t\r\n\t\t\t\t.replace(/ THEN /ig,\" THEN~::~\"+tab+\"\")\r\n\t\t\t\t.replace(/ UNION /ig,\"~::~UNION~::~\")\r\n\t\t\t\t.replace(/ USING /ig,\"~::~USING \")\r\n\t\t\t\t.replace(/ WHEN /ig,\"~::~\"+tab+\"WHEN \")\r\n\t\t\t\t.replace(/ WHERE /ig,\"~::~WHERE \")\r\n\t\t\t\t.replace(/ WITH /ig,\"~::~WITH \")\r\n\t\t\t\t\r\n\t\t\t\t//.replace(/\\,\\s{0,}\\(/ig,\",~::~( \")\r\n\t\t\t\t//.replace(/\\,/ig,\",~::~\"+tab+tab+\"\")\r\n\r\n\t\t\t\t.replace(/ ALL /ig,\" ALL \")\r\n\t\t\t\t.replace(/ AS /ig,\" AS \")\r\n\t\t\t\t.replace(/ ASC /ig,\" ASC \")\t\r\n\t\t\t\t.replace(/ DESC /ig,\" DESC \")\t\r\n\t\t\t\t.replace(/ DISTINCT /ig,\" DISTINCT \")\r\n\t\t\t\t.replace(/ EXISTS /ig,\" EXISTS \")\r\n\t\t\t\t.replace(/ NOT /ig,\" NOT \")\r\n\t\t\t\t.replace(/ NULL /ig,\" NULL \")\r\n\t\t\t\t.replace(/ LIKE /ig,\" LIKE \")\r\n\t\t\t\t.replace(/\\s{0,}SELECT /ig,\"SELECT \")\r\n\t\t\t\t.replace(/\\s{0,}UPDATE /ig,\"UPDATE \")\r\n\t\t\t\t.replace(/ SET /ig,\" SET \")\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t.replace(/~::~{1,}/g,\"~::~\")\r\n\t\t\t\t.split('~::~');\r\n}\r\n\r\nvkbeautify.prototype.sql = function(text,step) {\r\n\r\n\tvar ar_by_quote = text.replace(/\\s{1,}/g,\" \")\r\n\t\t\t\t\t\t\t.replace(/\\'/ig,\"~::~\\'\")\r\n\t\t\t\t\t\t\t.split('~::~'),\r\n\t\tlen = ar_by_quote.length,\r\n\t\tar = [],\r\n\t\tdeep = 0,\r\n\t\ttab = this.step,//+this.step,\r\n\t\tinComment = true,\r\n\t\tinQuote = false,\r\n\t\tparenthesisLevel = 0,\r\n\t\tstr = '',\r\n\t\tix = 0,\r\n\t\tshift = step ? createShiftArr(step) : this.shift;;\r\n\r\n\t\tfor(ix=0;ix<len;ix++) {\r\n\t\t\tif(ix%2) {\r\n\t\t\t\tar = ar.concat(ar_by_quote[ix]);\r\n\t\t\t} else {\r\n\t\t\t\tar = ar.concat(split_sql(ar_by_quote[ix], tab) );\r\n\t\t\t}\r\n\t\t}\r\n\t\t\r\n\t\tlen = ar.length;\r\n\t\tfor(ix=0;ix<len;ix++) {\r\n\t\t\t\r\n\t\t\tparenthesisLevel = isSubquery(ar[ix], parenthesisLevel);\r\n\t\t\t\r\n\t\t\tif( /\\s{0,}\\s{0,}SELECT\\s{0,}/.exec(ar[ix]))  { \r\n\t\t\t\tar[ix] = ar[ix].replace(/\\,/g,\",\\n\"+tab+tab+\"\")\r\n\t\t\t} \r\n\t\t\t\r\n\t\t\tif( /\\s{0,}\\s{0,}SET\\s{0,}/.exec(ar[ix]))  { \r\n\t\t\t\tar[ix] = ar[ix].replace(/\\,/g,\",\\n\"+tab+tab+\"\")\r\n\t\t\t} \r\n\t\t\t\r\n\t\t\tif( /\\s{0,}\\(\\s{0,}SELECT\\s{0,}/.exec(ar[ix]))  { \r\n\t\t\t\tdeep++;\r\n\t\t\t\tstr += shift[deep]+ar[ix];\r\n\t\t\t} else \r\n\t\t\tif( /\\'/.exec(ar[ix]) )  { \r\n\t\t\t\tif(parenthesisLevel<1 && deep) {\r\n\t\t\t\t\tdeep--;\r\n\t\t\t\t}\r\n\t\t\t\tstr += ar[ix];\r\n\t\t\t}\r\n\t\t\telse  { \r\n\t\t\t\tstr += shift[deep]+ar[ix];\r\n\t\t\t\tif(parenthesisLevel<1 && deep) {\r\n\t\t\t\t\tdeep--;\r\n\t\t\t\t}\r\n\t\t\t} \r\n\t\t\tvar junk = 0;\r\n\t\t}\r\n\r\n\t\tstr = str.replace(/^\\n{1,}/,'').replace(/\\n{1,}/g,\"\\n\");\r\n\t\treturn str;\r\n}\r\n\r\n\r\nvkbeautify.prototype.xmlmin = function(text, preserveComments) {\r\n\r\n\tvar str = preserveComments ? text\r\n\t\t\t\t\t\t\t   : text.replace(/\\<![ \\r\\n\\t]*(--([^\\-]|[\\r\\n]|-[^\\-])*--[ \\r\\n\\t]*)\\>/g,\"\")\r\n\t\t\t\t\t\t\t\t\t .replace(/[ \\r\\n\\t]{1,}xmlns/g, ' xmlns');\r\n\treturn  str.replace(/>\\s{0,}</g,\"><\"); \r\n}\r\n\r\nvkbeautify.prototype.jsonmin = function(text) {\r\n\r\n\tif (typeof JSON === 'undefined' ) return text; \r\n\t\r\n\treturn JSON.stringify(JSON.parse(text), null, 0); \r\n\t\t\t\t\r\n}\r\n\r\nvkbeautify.prototype.cssmin = function(text, preserveComments) {\r\n\t\r\n\tvar str = preserveComments ? text\r\n\t\t\t\t\t\t\t   : text.replace(/\\/\\*([^*]|[\\r\\n]|(\\*+([^*/]|[\\r\\n])))*\\*+\\//g,\"\") ;\r\n\r\n\treturn str.replace(/\\s{1,}/g,' ')\r\n\t\t\t  .replace(/\\{\\s{1,}/g,\"{\")\r\n\t\t\t  .replace(/\\}\\s{1,}/g,\"}\")\r\n\t\t\t  .replace(/\\;\\s{1,}/g,\";\")\r\n\t\t\t  .replace(/\\/\\*\\s{1,}/g,\"/*\")\r\n\t\t\t  .replace(/\\*\\/\\s{1,}/g,\"*/\");\r\n}\r\n\r\nvkbeautify.prototype.sqlmin = function(text) {\r\n\treturn text.replace(/\\s{1,}/g,\" \").replace(/\\s{1,}\\(/,\"(\").replace(/\\s{1,}\\)/,\")\");\r\n}\r\n\r\nwindow.vkbeautify = new vkbeautify();\r\n\r\n})();\r\n\r\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\n/**\n * @param {HTMLImageElement} img\n * @returns {Promise<Boolean>}\n */\nasync function _isImageCorsProtected(img) {\n    const src = img.getAttribute(\"src\");\n    if (!src) {\n        return false;\n    }\n    let isCorsProtected = false;\n    if (!src.startsWith(\"/\") || /\\/web\\/image\\/\\d+-redirect\\//.test(src)) {\n        // The `fetch()` used later in the code might fail if the image is\n        // CORS protected. We check upfront if it's the case.\n        // Two possible cases:\n        // 1. the `src` is an absolute URL from another domain.\n        //    For instance, abc.odoo.com vs abc.com which are actually the\n        //    same database behind.\n        // 2. A \"attachment-url\" which is just a redirect to the real image\n        //    which could be hosted on another website.\n        isCorsProtected = await fetch(src, { method: \"HEAD\" })\n            .then(() => false)\n            .catch(() => true);\n    }\n    return isCorsProtected;\n}\n\n/**\n * @param {string} src\n * @returns {Promise<Boolean>}\n */\nasync function _isSrcCorsProtected(src) {\n    const dummyImg = document.createElement(\"img\");\n    dummyImg.src = src;\n    return _isImageCorsProtected(dummyImg);\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    isImageCorsProtected: _isImageCorsProtected,\n    isSrcCorsProtected: _isSrcCorsProtected,\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}", "/** @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\", \"o_small_twelve-fs\", \"o_small_ten-fs\", \"o_small_eight-fs\"];\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    const isInsideColumn = closestElement(end, '.o_text_columns')\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        !isInsideColumn\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\nexport function 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    const lastInlineAncestor = ancestors.findLast((element) => !isBlock(element) && element.isConnected);\n    if (span) {\n        return span;\n    } else {\n        const span = document.createElement('span');\n        // Apply font span above current inline top ancestor so that \n        // the font style applies to the other style tags as well.\n        if (lastInlineAncestor) {\n            lastInlineAncestor.after(span);\n            span.append(lastInlineAncestor);\n        } else {\n            node.after(span);\n            span.append(node);\n        }\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    const selectedNodes = getSelectedNodes(editor.editable).filter(\n        (n) =>\n            ((n.nodeType === Node.TEXT_NODE && (isVisibleTextNode(n) || isZWS(n))) ||\n                n.nodeName === \"BR\") &&\n            closestElement(n).isContentEditable\n    );\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 node of selectedNodes) {\n        const inlineAncestors = [];\n        let currentNode = node;\n        let parentNode = node.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        const isFormatted =\n            formatName === \"setFontSizeClassName\" && !formatProps\n                ? hasAnyFontSizeClass\n                : formatSpec.isFormatted;\n        const firstBlockOrClassHasFormat = isFormatted(parentNode, formatProps);\n        if (firstBlockOrClassHasFormat && !applyStyle) {\n            formatSpec.addNeutralStyle && formatSpec.addNeutralStyle(getOrCreateSpan(node, inlineAncestors));\n        } else if (!firstBlockOrClassHasFormat && applyStyle) {\n            const tag = formatSpec.tagName && document.createElement(formatSpec.tagName);\n            if (tag) {\n                node.after(tag);\n                tag.append(node);\n\n                if (!isFormatted(tag, formatProps)) {\n                    tag.after(node);\n                    tag.remove();\n                    formatSpec.addStyle(getOrCreateSpan(node, inlineAncestors), formatProps);\n                }\n            } else if (formatName !== 'fontSize' || formatProps.size !== undefined) {\n                formatSpec.addStyle(getOrCreateSpan(node, 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            selectedNodes.includes(siblings[0]) &&\n            selectedNodes.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    if (selectedNodes.length === 1 && selectedNodes[0].textContent === '\\u200B') {\n        setSelection(selectedNodes[0], 0);\n    } else if (selectedNodes.length) {\n        const firstNode = selectedNodes[0];\n        const lastNode = selectedNodes[selectedNodes.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.parentElement.isContentEditable && 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/**\n * Return true if the given node has any font-size class.\n *\n * @param {Node} node A node to check for font-size classes.\n * @returns {boolean}\n */\nexport function hasAnyFontSizeClass(node) {\n    return FONT_SIZE_CLASSES.find((cls) => node?.classList?.contains(cls));\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 */\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 =\n        format === \"setFontSizeClassName\" ? hasAnyFontSizeClass : 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.matches(\".oe_unbreakable, a.btn, a[role='tab'], a[role='button'], li.nav-item\")\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) && !['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6'].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}\n\n/**\n * Returns whether an element is a button\n *\n * @param {Node} node\n * @returns {boolean}\n */\nexport function isButton(node) {\n    if (!node || node.nodeType !== Node.ELEMENT_NODE) {\n        return false;\n    }\n    return node.nodeName === \"BUTTON\" || node.classList.contains(\"btn\");\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\", \"lead\"];\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    const pxValue = remValue && 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\") && isEmptyBlock(el)) {\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 isRoot = node => node.oid === \"root\";\n\nexport const leftLeafFirstPath = createDOMPathGenerator(DIRECTIONS.LEFT);\nexport const leftLeafOnlyNotBlockPath = createDOMPathGenerator(DIRECTIONS.LEFT, {\n    leafOnly: true,\n    stopTraverseFunction: isBlock,\n    stopFunction: node => isBlock(node) || isRoot(node),\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) || isRoot(node),\n});\n\nexport const rightLeafOnlyNotBlockPath = createDOMPathGenerator(DIRECTIONS.RIGHT, {\n    leafOnly: true,\n    stopTraverseFunction: isBlock,\n    stopFunction: node => isBlock(node) || isRoot(node),\n});\n\nexport const rightLeafOnlyPathNotBlockNotEditablePath = createDOMPathGenerator(DIRECTIONS.RIGHT, {\n    leafOnly: true,\n    stopFunction: node => isRoot(node),\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) || isRoot(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) || isRoot(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) => {\n            node.nodeValue = node.nodeValue.replace(/\\u200B/g, \"\");\n\n            // If a node becomes empty after removing ZWS, remove it.\n            if (node.nodeValue === \"\") {\n                node.remove();\n            }\n        });\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 { _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        const fileInputEl = this.fileInput.el;\n        if (fileInputEl) {\n            fileInputEl.value = \"\";\n        }\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        this.isImageField = !!(this.props.media && this.props.media.closest(\"[data-oe-type=image]\")) || !!this.env.addFieldImage;\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                    const urlPathname = new URL(url, window.location.href).pathname;\n                    const imageExtension = IMAGE_EXTENSIONS.find(format => urlPathname.endsWith(format));\n                    if (this.isImageField && imageExtension === \".webp\") {\n                        // Do not allow the user to replace an image field by a\n                        // webp CORS protected image as we are not currently\n                        // able to manage the report creation if such images are\n                        // in there (as the equivalent jpeg can not be\n                        // generated). It also causes a problem for resize\n                        // operations as 'libwep' can not be used.\n                        this.notificationService.add(_t(\n                            \"You can not replace a field by this image. If you want to use this image, first save it on your computer and then upload it here.\"\n                        ), {\n                            title: _t(\"Error\"),\n                            sticky: true,\n                        });\n                        return resolve();\n                    }\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        if (this.isImageField) {\n            // The image is a field; mark the attachments if they are linked to\n            // a webp CORS protected image. Indeed, in this case, they should\n            // not be selectable on the media dialog (due to a problem of image\n            // resize and report creation).\n            for (const attachment of attachments) {\n                if (attachment.mimetype === \"image/webp\" && await weUtils.isSrcCorsProtected(attachment.image_src)) {\n                    attachment.unselectable = true;\n                }\n            }\n        }\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        if (attachment.unselectable) {\n            this.notificationService.add(_t(\n                \"You can not replace a field by this image. If you want to use this image, first save it on your computer and then upload it here.\"\n            ), {\n                title: _t(\"Error\"),\n                sticky: true,\n            });\n            return;\n        }\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            const contentType = response.headers.get(\"content-type\");\n            if (contentType && contentType.startsWith(\"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                } else if ([TABS.VIDEOS.id, TABS.DOCUMENTS.id].includes(this.state.activeTab)) {\n                    const parentEl = this.props.media.parentElement;\n                    if (\n                        parentEl &&\n                        parentEl.tagName === \"A\" &&\n                        parentEl.children.length === 1 &&\n                        this.props.media.tagName === \"IMG\"\n                    ) {\n                        // If an image is wrapped in an <a> tag, we remove the link when replacing it with a video or document\n                        parentEl.replaceWith(parentEl.firstElementChild);\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                await this.props.save(elements);\n            } else {\n                await 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 () => this.prepareVimeoPreviews());\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    /**\n     * Based on the config vimeo ids, prepare the vimeo previews.\n     */\n    async prepareVimeoPreviews() {\n        return 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", "/** @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", "/** This patch is no longer needed, and will be removed in master */\n\nimport { 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\nimport { ancestors } from '@web_editor/js/common/wysiwyg_utils';\nimport { closestElement } from \"@web_editor/js/editor/odoo-editor/src/utils/utils\";\n\nexport class QWebPlugin {\n    constructor(options = {}) {\n        this._options = options;\n        if (this._options.editor) {\n            this._editable = this._options.editor.editable;\n            this._document = this._options.editor.document;\n            this._selectQwebNode(this._options.editor);\n        } else {\n            this._editable = this._options.editable;\n            this._document = this._options.document || window.document;\n        }\n        this._getContextFromParentRect = this._options.editor?.options?.getContextFromParentRect || (() => ({ top: 0, left: 0 }));\n        this._editable = this._options.editable || (this._options.editor && this._options.editor.editable);\n        this._document = this._options.document || (this._options.editor && this._options.editor.document) || window.document;\n        this._tGroupCount = 0;\n        this._hideBranchingSelection = this._hideBranchingSelection.bind(this);\n        this._makeBranchingSelection();\n        this._clickListeners = [];\n    }\n    destroy() {\n        this._selectElWrapper.remove();\n        for (const listener of this._clickListeners) {\n            document.removeEventListener('click', listener);\n        }\n    }\n    cleanForSave(editable) {\n        for (const node of editable.querySelectorAll('[data-oe-t-group], [data-oe-t-inline], [data-oe-t-selectable], [data-oe-t-group-active]')) {\n            node.removeAttribute('data-oe-t-group-active');\n            node.removeAttribute('data-oe-t-group');\n            node.removeAttribute('data-oe-t-inline');\n            node.removeAttribute('data-oe-t-selectable');\n        }\n    }\n    sanitizeElement(subRoot) {\n        if (subRoot.nodeType !== Node.ELEMENT_NODE) {\n            return;\n        }\n        if (this._options.editor) {\n            this._options.editor.observerUnactive('qweb-plugin-sanitize');\n        }\n\n        this._fixInlines(subRoot);\n\n        const demoElements = subRoot.querySelectorAll('[t-esc], [t-raw], [t-out], [t-field]');\n        for (const element of demoElements) {\n            element.setAttribute('contenteditable', 'false');\n        }\n\n        this._groupQwebBranching(subRoot);\n        if (this._options.editor) {\n            this._options.editor.observerActive('qweb-plugin-sanitize');\n        }\n    }\n    _groupQwebBranching(subRoot) {\n        const tNodes = subRoot.querySelectorAll('[t-if], [t-elif], [t-else]');\n        const groupsEncounter = new Set();\n        for (const node of tNodes) {\n            const parentTNode = [...node.parentElement.children];\n            const index = parentTNode.indexOf(node);\n            const prevNode = parentTNode[index - 1];\n\n            let groupId;\n            if (\n                prevNode &&\n                node.previousElementSibling === prevNode &&\n                !node.hasAttribute('t-if')\n            ) {\n                // Make the first t-if selectable, if prevNode is not a t-if,\n                // it's already data-oe-t-selectable.\n                prevNode.setAttribute('data-oe-t-selectable', 'true');\n                groupId = parseInt(prevNode.getAttribute('data-oe-t-group'));\n                node.setAttribute('data-oe-t-selectable', 'true');\n            } else {\n                groupId = this._tGroupCount++;\n            }\n            groupsEncounter.add(groupId);\n            node.setAttribute('data-oe-t-group', groupId);\n\n            const clickListener = e => {\n                e.stopImmediatePropagation();\n                this._showBranchingSelection(node);\n            };\n            this._clickListeners.push(clickListener);\n            node.addEventListener('click', clickListener);\n        }\n        for (const groupId of groupsEncounter) {\n            const isOneElementActive = subRoot.querySelector(\n                `[data-oe-t-group='${groupId}'][data-oe-t-group-active]`,\n            );\n            // If there is no element in groupId activated, activate the first\n            // one.\n            if (!isOneElementActive) {\n                subRoot\n                    .querySelector(`[data-oe-t-group='${groupId}']`)\n                    .setAttribute('data-oe-t-group-active', 'true');\n            }\n        }\n    }\n    _fixInlines(subRoot) {\n        const checkAllInline = el => {\n            return [...el.children].every(child => {\n                if (child.tagName === 'T') {\n                    return checkAllInline(child);\n                } else {\n                    return (\n                        child.nodeType !== Node.ELEMENT_NODE ||\n                        window.getComputedStyle(child).display === 'inline'\n                    );\n                }\n            });\n        };\n        const tElements = subRoot.querySelectorAll('t');\n        // Wait for the content to be on the dom to check checkAllInline\n        // otherwise the getComputedStyle will be wrong.\n        // todo: remove the setTimeout when the editor will provide a signal\n        // that the editable is on the dom.\n        setTimeout(() => {\n            if (this._options.editor) {\n                this._options.editor.observerUnactive('qweb-plugin-checkAllInline');\n            }\n            for (const tElement of tElements) {\n                if (checkAllInline(tElement)) {\n                    tElement.setAttribute('data-oe-t-inline', 'true');\n                }\n            }\n            if (this._options.editor) {\n                this._options.editor.observerActive('qweb-plugin-checkAllInline');\n            }\n        });\n    }\n    _selectQwebNode(editor) {\n        editor.addDomListener(editor.document, 'selectionchange', e => {\n            const selection = editor.document.getSelection();\n            const qwebNode = selection.anchorNode && closestElement(selection.anchorNode, '[t-field],[t-esc],[t-out]');\n            if (qwebNode){\n                const range = new Range();\n                range.selectNode(qwebNode);\n                selection.removeAllRanges();\n                selection.addRange(range);\n            }\n        });\n    }\n    _makeBranchingSelection() {\n        const document = this._options.document || window.document;\n        this._selectElWrapper = document.createElement('div');\n        this._selectElWrapper.classList.add('oe-qweb-select');\n        this._selectElWrapper.innerHTML = '';\n        document.body.append(this._selectElWrapper);\n        this._hideBranchingSelection();\n    }\n    _showBranchingSelection(target) {\n        this._hideBranchingSelection();\n\n        const branchingHierarchyElements = [target, ...ancestors(target, this._editable)]\n            .filter(element => element.getAttribute('data-oe-t-group-active') === 'true')\n            .filter(element => {\n                const itemGroupId = element.getAttribute('data-oe-t-group');\n\n                const groupItemsNodes = element.parentElement.querySelectorAll(\n                    `[data-oe-t-group='${itemGroupId}']`,\n                );\n                return groupItemsNodes.length > 1;\n            });\n\n        if (!branchingHierarchyElements.length) return;\n\n        const groupsActive = branchingHierarchyElements.map(node =>\n            node.getAttribute('data-oe-t-group'),\n        );\n        for (const branchingElement of branchingHierarchyElements) {\n            this._selectElWrapper.prepend(this._renderBranchingSelection(branchingElement));\n        }\n        const closeSelectHandler = event => {\n            const path = [event.target, ...ancestors(event.target)];\n            const shouldClose = !path.find(\n                element =>\n                    element === this._selectElWrapper ||\n                    groupsActive.includes(element.getAttribute('data-oe-t-group')),\n            );\n            if (shouldClose) {\n                this._hideBranchingSelection();\n                document.removeEventListener('mousedown', closeSelectHandler);\n            }\n        };\n        document.addEventListener('mousedown', closeSelectHandler);\n        this._selectElWrapper.style.display = 'flex';\n        this._updateBranchingSelectionPosition(\n            branchingHierarchyElements[branchingHierarchyElements.length - 1],\n        );\n    }\n    _updateBranchingSelectionPosition(target) {\n        const box = target.getBoundingClientRect();\n        const selBox = this._selectElWrapper.getBoundingClientRect();\n        const parentBox = this._getContextFromParentRect();\n\n        const left = parentBox.left + window.scrollX + box.left;\n        const top = parentBox.top + window.scrollY + box.top - selBox.height;\n\n        this._selectElWrapper.style.left = `${left}px`;\n        this._selectElWrapper.style.top = `${top}px`;\n    }\n    _renderBranchingSelection(target) {\n        this._document.addEventListener('scroll', this._hideBranchingSelection);\n        const selectEl = document.createElement('select');\n        const groupId = parseInt(target.getAttribute('data-oe-t-group'));\n        const groupElements = target.parentElement.querySelectorAll(\n            `[data-oe-t-group='${groupId}']`,\n        );\n        for (const element of groupElements) {\n            const optionElement = document.createElement('option');\n            if (element.hasAttribute('t-if')) {\n                optionElement.innerText = `if: \"${element.getAttribute(\"t-if\")}\"`;\n            } else if (element.hasAttribute('t-elif')) {\n                optionElement.innerText = `elif: \"${element.getAttribute(\"t-elif\")}\"`;\n            } else if (element.hasAttribute('t-else')) {\n                optionElement.innerText = 'else';\n            }\n            if (element.hasAttribute('data-oe-t-group-active')) {\n                optionElement.selected = true;\n            }\n            selectEl.appendChild(optionElement);\n        }\n\n        selectEl.onchange = () => {\n            let activeElement;\n            for (let i = 0; i < groupElements.length; i++) {\n                if (i === selectEl.selectedIndex) {\n                    activeElement = groupElements[i];\n                    groupElements[i].setAttribute('data-oe-t-group-active', 'true');\n                } else {\n                    groupElements[i].removeAttribute('data-oe-t-group-active');\n                }\n            }\n            this._showBranchingSelection(activeElement);\n        };\n        return selectEl;\n    }\n    _hideBranchingSelection() {\n        this._selectElWrapper.style.display = 'none';\n        this._selectElWrapper.innerHTML = ``;\n        this._document.removeEventListener('scroll', this._hideBranchingSelection);\n    }\n}\n", "/** @odoo-module */\n\nimport { getAdjacentPreviousSiblings, isBlock, rgbToHex, commonParentGet } from '../editor/odoo-editor/src/utils/utils';\n\n//--------------------------------------------------------------------------\n// Constants\n//--------------------------------------------------------------------------\n\nconst RE_COL_MATCH = /(^| )col(-[\\w\\d]+)*( |$)/;\nconst RE_COMMAS_OUTSIDE_PARENTHESES = /,(?![^(]*?\\))/g;\nconst RE_OFFSET_MATCH = /(^| )offset(-[\\w\\d]+)*( |$)/;\nconst RE_PADDING_MATCH = /[ ]*padding[^;]*;/g;\nconst RE_PADDING = /([\\d.]+)/;\nconst RE_WHITESPACE = /[\\s\\u200b]*/;\nconst SELECTORS_IGNORE = /(^\\*$|:hover|:before|:after|:active|:link|::|'|\\([^(),]+[,(])|@page/;\n// CSS properties relating to font, which Outlook seem to have trouble inheriting.\nconst FONT_PROPERTIES_TO_INHERIT = [\n    'color',\n    'font-size',\n    'font-family',\n    'font-weight',\n    'font-style',\n    'text-decoration',\n    'text-transform',\n    'text-align',\n];\n// Attributes all tables should have in a mailing.\nexport const TABLE_ATTRIBUTES = {\n    cellspacing: 0,\n    cellpadding: 0,\n    border: 0,\n    width: '100%',\n    align: 'center',\n    role: 'presentation',\n};\n// Cancel tables default styles.\nexport const TABLE_STYLES = {\n    'border-collapse': 'collapse',\n    'text-align': 'inherit',\n    'font-size': 'unset',\n    'line-height': 'inherit',\n};\n\nconst GROUPED_STYLES = {\n    border: [\n        \"border-top-width\", \"border-right-width\", \"border-bottom-width\", \"border-left-width\",\n        \"border-top-style\", \"border-right-style\", \"border-bottom-style\", \"border-left-style\",\n    ],\n    padding: [\"padding-top\", \"padding-bottom\", \"padding-left\", \"padding-right\"],\n    margin: [\"margin-top\", \"margin-bottom\", \"margin-left\", \"margin-right\"],\n    \"border-radius\": [\n        \"border-top-left-radius\", \"border-top-right-radius\",\n        \"border-bottom-right-radius\", \"border-bottom-left-radius\",\n    ],\n};\n\n//--------------------------------------------------------------------------\n// Public\n//--------------------------------------------------------------------------\n\n/**\n * Convert snippets and mailing bodies to tables.\n *\n * @param {JQuery} $editable\n */\nfunction addTables($editable) {\n    const editable = $editable.get(0);\n    for (const snippet of editable.querySelectorAll('.o_mail_snippet_general, .o_layout')) {\n        // Convert all snippets and the mailing itself into table > tr > td\n        const table = _createTable(snippet.attributes);\n\n        const row = document.createElement('tr');\n        let col = document.createElement('td');\n        row.appendChild(col);\n        if (snippet.classList.contains('o_basic_theme')) {\n            const div = document.createElement('div');\n            div.classList.add('o_apple_wrapper_padding');\n            col.appendChild(div);\n            col = div;\n            const style = document.createElement('style');\n            // We create a nested media query because it's only supported by a\n            // handful of clients, including Apple Mail, and we actually only\n            // want this for Apple Mail.\n            const padding = '34px'; // This is what's needed to align the content with Apple Mail's header.\n            style.textContent = `@media{@media{.o_basic_theme div.o_apple_wrapper_padding{padding:${snippet.style.padding};}}}` +\n                `@media(min-width:961px){@media{@media{.o_basic_theme div.o_apple_wrapper_padding{padding-left:${padding};}}}}`;\n            div.before(style);\n        }\n        table.appendChild(row);\n\n        for (const child of [...snippet.childNodes]) {\n            col.appendChild(child);\n        }\n        snippet.before(table);\n        snippet.remove();\n\n        // If snippet doesn't have a table as child, wrap its contents in one.\n        const childTables = [...col.children].filter(child => child.nodeName === 'TABLE');\n        if (!childTables.length) {\n            const tableB = _createTable();\n            const rowB = document.createElement('tr');\n            const colB = document.createElement('td');\n\n            rowB.appendChild(colB);\n            tableB.appendChild(rowB);\n            for (const child of [...col.childNodes]) {\n                colB.appendChild(child);\n            }\n            col.appendChild(tableB);\n        }\n    }\n}\n/**\n * Convert CSS display for attachment link to real image.\n * Without this post process, the display depends on the CSS and the picture\n * does not appear when we use the html without css (to send by email for e.g.)\n *\n * @param {JQuery} $editable\n */\nfunction attachmentThumbnailToLinkImg($editable) {\n    const editable = $editable.get(0);\n    const links = [...editable.querySelectorAll(`a[href*=\"/web/content/\"][data-mimetype]:empty`)].filter(link => (\n        RE_WHITESPACE.test(link.textContent)\n    ));\n    for (const link of links) {\n        const image = document.createElement('img');\n        image.setAttribute('src', _getStylePropertyValue(link, 'background-image').replace(/(^url\\(['\"])|(['\"]\\)$)/g, ''));\n        // Note: will trigger layout thrashing.\n        image.setAttribute('height', Math.max(1, _getHeight(link)));\n        image.setAttribute('width', Math.max(1, _getWidth(link)));\n        link.prepend(image);\n    };\n}\n/**\n * Convert Bootstrap rows and columns to actual tables.\n *\n * Note: Because of the limited support of media queries in emails, this doesn't\n * support the mixing and matching of column options (e.g., \"col-4 col-sm-6\" and\n * \"col col-4\" aren't supported).\n *\n * @param {Element} editable\n */\nfunction bootstrapToTable(editable) {\n    // First give all rows in columns a separate container parent.\n    for (const rowInColumn of [...editable.querySelectorAll('.row')].filter(row => RE_COL_MATCH.test(row.parentElement.className))) {\n        const parentColumn = rowInColumn.parentElement;\n        const previous = rowInColumn.previousElementSibling;\n        if (previous && previous.classList.contains('o_fake_table')) {\n            // If a container was already created there, append to it.\n            previous.append(rowInColumn);\n        } else {\n            _wrap(rowInColumn, 'div', 'o_fake_table');\n        }\n        // Bootstrap rows have negative left and right margins, which are not\n        // supported by GMail and Outlook. Add up the padding of the column with\n        // the negative margin of the row to get the correct padding.\n        const rowStyle = getComputedStyle(rowInColumn);\n        const columnStyle = getComputedStyle(parentColumn);\n        for (const side of ['left', 'right']) {\n            const negativeMargin = +rowStyle[`margin-${side}`].replace('px', '');\n            const columnPadding = +columnStyle[`padding-${side}`].replace('px', '');\n            if (negativeMargin < 0 && columnPadding >= Math.abs(negativeMargin)) {\n                parentColumn.style[`padding-${side}`] = `${columnPadding + negativeMargin}px`;\n                rowInColumn.style[`margin-${side}`] = 0;\n            }\n        }\n    }\n\n    // These containers from the mass mailing masonry snippet require full\n    // height contents, which is only possible if the table itself has a set\n    // height. We also need to restyle it because of the change in structure.\n    for(const masonryTopInnerContainer of editable.querySelectorAll('.s_masonry_block > .container')) {\n        masonryTopInnerContainer.style.setProperty('height', '100%');\n    }\n    for (const masonryGrid of editable.querySelectorAll('.o_masonry_grid_container')) {\n        masonryGrid.style.setProperty('padding', 0);\n        for (const fakeTable of [...masonryGrid.children].filter(c => c.classList.contains('o_fake_table'))) {\n            fakeTable.style.setProperty('height', _getHeight(fakeTable) + 'px');\n        }\n    }\n    for (const masonryRow of editable.querySelectorAll('.o_masonry_grid_container > .o_fake_table > .row.h-100')) {\n        masonryRow.style.removeProperty('height');\n        masonryRow.parentElement.style.setProperty('height', '100%');\n    }\n\n    const containers = editable.querySelectorAll('.container, .container-fluid, .o_fake_table');\n    // Capture the widths of the containers before manipulating it.\n    for (const container of containers) {\n        container.setAttribute('o-temp-width', _getWidth(container));\n    }\n    // Now convert all containers with rows to tables.\n    for (const container of [...containers].filter(n => [...n.children].some(c => c.classList.contains('row')))) {\n        // The width of the table was stored in a temporary attribute. Fetch it\n        // for use in `_applyColspan` and remove the attribute at the end.\n        const containerWidth = parseFloat(container.getAttribute('o-temp-width'));\n\n        // TABLE\n        const table = _createTable(container.attributes);\n        for (const child of [...container.childNodes]) {\n            table.append(child);\n        }\n        table.classList.remove('container', 'container-fluid', 'o_fake_table');\n        if (!table.className) {\n            table.removeAttribute('class');\n        }\n        container.before(table);\n        container.remove();\n\n\n        // ROWS\n        // First give all siblings of rows a separate row/col parent combo.\n        for (const row of [...table.children].filter(child => isBlock(child) && !child.classList.contains('row'))) {\n            const newCol = _wrap(row, 'div', 'col-12');\n            _wrap(newCol, 'div', 'row');\n        }\n\n        for (const bootstrapRow of [...table.children].filter(c => c.classList.contains('row'))) {\n            const tr = document.createElement('tr');\n            for (const attr of bootstrapRow.attributes) {\n                tr.setAttribute(attr.name, attr.value);\n            }\n            tr.classList.remove('row');\n            if (!tr.className) {\n                tr.removeAttribute('class');\n            }\n            for (const child of [...bootstrapRow.childNodes]) {\n                tr.append(child);\n            }\n            bootstrapRow.before(tr);\n            bootstrapRow.remove();\n\n\n            // COLUMNS\n            const bootstrapColumns = [...tr.children].filter(column => column.className && column.className.match(RE_COL_MATCH));\n\n            // 1. Replace generic \"col\" classes with specific \"col-n\", computed\n            //    by sharing the available space between them.\n            const flexColumns = bootstrapColumns.filter(column => !/\\d/.test(column.className.match(RE_COL_MATCH)[0] || '0'));\n            const colTotalSize = bootstrapColumns.map(child => _getColumnSize(child) + _getColumnOffsetSize(child)).reduce((a, b) => a + b, 0);\n            const colSize = Math.max(1, Math.round((12 - colTotalSize) / flexColumns.length));\n            for (const flexColumn of flexColumns) {\n                flexColumn.classList.remove(flexColumn.className.match(RE_COL_MATCH)[0].trim());\n                flexColumn.classList.add(`col-${colSize}`);\n            }\n\n            // 2. Create and fill up the row(s) with grid(s).\n            // Create new, empty columns for column offsets.\n            let columnIndex = 0;\n            for (const bootstrapColumn of [...bootstrapColumns]) {\n                const offsetSize = _getColumnOffsetSize(bootstrapColumn);\n                if (offsetSize) {\n                    const newColumn = document.createElement('div');\n                    newColumn.classList.add(`col-${offsetSize}`);\n                    bootstrapColumn.classList.remove(bootstrapColumn.className.match(RE_OFFSET_MATCH)[0].trim());\n                    bootstrapColumn.before(newColumn);\n                    bootstrapColumns.splice(columnIndex, 0, newColumn);\n                    columnIndex++;\n                }\n                columnIndex++;\n            }\n            let grid = _createColumnGrid();\n            let gridIndex = 0;\n            let currentRow = tr.cloneNode();\n            tr.after(currentRow);\n            let currentCol;\n            columnIndex = 0;\n            for (const bootstrapColumn of bootstrapColumns) {\n                const columnSize = _getColumnSize(bootstrapColumn);\n                if (gridIndex + columnSize < 12) {\n                    currentCol = grid[gridIndex];\n                    _applyColspan(currentCol, columnSize, containerWidth);\n                    gridIndex += columnSize;\n                    if (columnIndex === bootstrapColumns.length - 1) {\n                        // We handled all the columns but there is still space\n                        // in the row. Insert the columns and fill the row.\n                        _applyColspan(grid[gridIndex], 12 - gridIndex, containerWidth);\n                        currentRow.append(...grid.filter(td => td.getAttribute('colspan')));\n                    }\n                } else if (gridIndex + columnSize === 12) {\n                    // Finish the row.\n                    currentCol = grid[gridIndex];\n                    if (currentCol) {\n                        _applyColspan(currentCol, columnSize, containerWidth);\n                    }\n                    currentRow.append(...grid.filter(td => td.getAttribute('colspan')));    \n                    if (columnIndex !== bootstrapColumns.length - 1) {\n                        // The row was filled before we handled all of its\n                        // columns. Create a new one and start again from there.\n                        const previousRow = currentRow;\n                        currentRow = currentRow.cloneNode();\n                        previousRow.after(currentRow);\n                        grid = _createColumnGrid();\n                        gridIndex = 0;\n                    }\n                } else {\n                    // Fill the row with what was in the grid before it\n                    // overflowed.\n                    if (grid[gridIndex]) {\n                        _applyColspan(grid[gridIndex], 12 - gridIndex, containerWidth);\n                    }\n                    currentRow.append(...grid.filter(td => td.getAttribute('colspan')));\n                    // Start a new row that starts with the current col.\n                    const previousRow = currentRow;\n                    currentRow = currentRow.cloneNode();\n                    previousRow.after(currentRow);\n                    grid = _createColumnGrid();\n                    currentCol = grid[0];\n                    _applyColspan(currentCol, columnSize, containerWidth);\n                    gridIndex = columnSize;\n                    if (columnIndex === bootstrapColumns.length - 1 && gridIndex < 12) {\n                        // We handled all the columns but there is still space\n                        // in the row. Insert the columns and fill the row.\n                        _applyColspan(grid[gridIndex], 12 - gridIndex, containerWidth);\n                        currentRow.append(...grid.filter(td => td.getAttribute('colspan')));\n                    }\n                }\n                if (currentCol) {\n                    for (const attr of bootstrapColumn.attributes) {\n                        if (attr.name !== 'colspan') {\n                            currentCol.setAttribute(attr.name, attr.value);\n                        }\n                    }\n                    const colMatch = bootstrapColumn.className.match(RE_COL_MATCH);\n                    currentCol.classList.remove(colMatch[0].trim());\n                    if (!currentCol.className) {\n                        currentCol.removeAttribute('class');\n                    }\n                    for (const child of [...bootstrapColumn.childNodes]) {\n                        currentCol.append(child);\n                    }\n                    // Adapt width to colspan.\n                    _applyColspan(currentCol, +currentCol.getAttribute('colspan'), containerWidth);\n                }\n                columnIndex++;\n            }\n            tr.remove(); // row was cloned and inserted already\n        }\n    }\n    for (const table of editable.querySelectorAll('table')) {\n        table.removeAttribute('o-temp-width');\n    }\n    // Merge tables in tds into one common table, each in its own row.\n    const tds = [...editable.querySelectorAll('td')]\n        .filter(td => td.children.length > 1 && [...td.children].every(child => child.nodeName === 'TABLE'))\n        .reverse();\n    for (const td of tds) {\n        const table = _createTable();\n        const trs = [...td.children].map(child => _wrap(child, 'td')).map(wrappedChild => _wrap(wrappedChild, 'tr'));\n        trs[0].before(table);\n        table.append(...trs);\n    }\n}\n/**\n * Convert Bootstrap cards to table structures.\n *\n * @param {Element} editable\n */\nfunction cardToTable(editable) {\n    for (const card of editable.querySelectorAll('.card')) {\n        const table = _createTable(card.attributes);\n        table.style.removeProperty('overflow');\n        const cardImgTopSuperRows = [];\n        for (const child of [...card.childNodes]) {\n            const row = document.createElement('tr');\n            const col = document.createElement('td');\n            if (!['IMG', 'A'].includes(child.nodeName) && isBlock(child)) {\n                for (const attr of child.attributes) {\n                    col.setAttribute(attr.name, attr.value);\n                }\n                for (const descendant of [...child.childNodes]) {\n                    col.append(descendant);\n                }\n                child.remove();\n            } else if (child.nodeType === Node.TEXT_NODE) {\n                if (child.textContent.replace(RE_WHITESPACE, '').length) {\n                    col.append(child);\n                } else {\n                    continue;\n                }\n            } else {\n                col.append(child);\n            }\n            const subTable = _createTable();\n            const superRow = document.createElement('tr');\n            const superCol = document.createElement('td');\n            row.append(col);\n            subTable.append(row);\n            superCol.append(subTable);\n            superRow.append(superCol);\n            table.append(superRow);\n            if (child.nodeType === Node.ELEMENT_NODE) {\n                const hasImgTop = [child, ...child.querySelectorAll('.card-img-top')].some(node => (\n                    node.classList && node.classList.contains('card-img-top') && node.closest && node.closest('.card') === table\n                ));\n                if (hasImgTop) {\n                    // Collect .card-img-top superRows to manipulate their heights.\n                    cardImgTopSuperRows.push(superRow);\n                }\n            }\n        }\n        // We expect successive .card-img-top to have the same height so the\n        // bodies of the cards are aligned. This achieves that without flexboxes\n        // by forcing the height of the smallest card:\n        const smallestCardImgRow = Math.min(0, ...cardImgTopSuperRows.map(row => row.clientHeight));\n        for (const row of cardImgTopSuperRows) {\n            row.style.height = smallestCardImgRow + 'px';\n        }\n        card.before(table);\n        card.remove();\n    }\n}\n/**\n * Convert CSS style to inline style (leave the classes on elements but forces\n * the style they give as inline style).\n *\n * @param {JQuery} $editable\n * @param {Object} cssRules\n */\nfunction classToStyle($editable, cssRules) {\n    const editable = $editable.get(0);\n    const writes = [];\n    const nodeToRules = new Map();\n    const rulesToProcess = [];\n    for (const rule of cssRules) {\n        const nodes = editable.querySelectorAll(rule.selector);\n        if (nodes.length) {\n            rulesToProcess.push(rule);\n        }\n        for (const node of nodes) {\n            const nodeRules = nodeToRules.get(node);\n            if (!nodeRules) {\n                nodeToRules.set(node, [rule]);\n            } else {\n                nodeRules.push(rule);\n            }\n        }\n    }\n    _computeStyleAndSpecificityOnRules(rulesToProcess);\n    for (const rules of nodeToRules.values()) {\n        rules.sort((a, b) => a.specificity - b.specificity);\n    }\n\n    for (const node of nodeToRules.keys()) {\n        const nodeRules = nodeToRules.get(node);\n        const css = nodeRules ? _getMatchedCSSRules(node, nodeRules, true) : {};\n        // Flexbox\n        for (const styleName of node.style) {\n            if (styleName.includes('flex') || `${node.style[styleName]}`.includes('flex')) {\n                writes.push(() => { node.style[styleName] = ''; });\n            }\n        }\n\n        // Do not apply css that would override inline styles (which are prioritary).\n        let style = node.getAttribute('style') || '';\n        // Outlook doesn't support inline !important\n        style = style.replace(/!important/g,'');\n        for (const [key, value] of Object.entries(css)) {\n            if (!(new RegExp(`(^|;)\\\\s*${key}[ :]`).test(style))) {\n                style = `${key}:${value};${style}`;\n            }\n        };\n        style = correctBorderAttributes(style);\n        if (Object.keys(style || {}).length === 0) {\n            writes.push(() => { node.removeAttribute('style'); });\n        } else {\n            writes.push(() => {\n                node.setAttribute('style', style);\n                if (node.style.width) {\n                    node.setAttribute('width', node.style.width.replace('px', '').trim());\n                }\n            });\n        }\n\n        if (node.nodeName === 'IMG') {\n            writes.push(() => {\n                // Media list images should not have an inline height\n                if (node.classList.contains('s_media_list_img')) {\n                    node.style.removeProperty('height');\n                }\n                // Protect aspect ratio when resizing in mobile.\n                if (node.style.getPropertyValue('width') === '100%' && node.style.getPropertyValue('object-fit') === '') {\n                    node.style.setProperty('object-fit', 'cover');\n                }\n            });\n        }\n        // Apple Mail\n        if (node.nodeName === 'TD' && !node.childNodes.length) {\n            // Append non-breaking spaces to empty table cells.\n            writes.push(() => { node.appendChild(document.createTextNode('\\u00A0')); });\n        }\n        // Outlook\n        if (node.nodeName === 'A' && node.classList.contains('btn') && !node.classList.contains('btn-link') && !node.children.length) {\n            writes.push(() => {\n                node.before(_createMso(`<table align=\"center\" border=\"0\"\n                    role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\"\n                    style=\"border-radius: 6px; border-collapse: separate !important;\">\n                        <tbody>\n                            <tr>\n                                <td style=\"${node.style.cssText.replace(RE_PADDING_MATCH, '').replaceAll('\"', '&quot;')}\" ${\n                                    node.parentElement.style.textAlign === 'center' ? 'align=\"center\" ' : ''\n                                }bgcolor=\"${rgbToHex(node.style.backgroundColor)}\">\n                    `));\n                node.after(_createMso(`</td>\n                        </tr>\n                    </tbody>\n                </table>`));\n            });\n        } else if (node.nodeName === 'IMG' && node.classList.contains('mx-auto') && node.classList.contains('d-block')) {\n            writes.push(() => { _wrap(node, 'p', 'o_outlook_hack', 'text-align:center;margin:0'); });\n        }\n\n        // Compute dynamic styles (var, calc).\n        writes.push(() => {\n            let computedStyle;\n            for (const styleName of node.style) {\n                const styleValue = node.style.getPropertyValue(styleName);\n                if (styleValue.includes('var(') || styleValue.includes('calc(')) {\n                    computedStyle = computedStyle || getComputedStyle(node);\n                    const prop = styleValue.includes('var(') ? styleValue.replace(/var\\((.*)\\)/, '$1') : styleName;\n                    const value = computedStyle.getPropertyValue(prop) || computedStyle.getPropertyValue(styleName);\n                    node.style.setProperty(styleName, value);\n                }\n            }\n        });\n\n        // Fix inheritance of font properties on Outlook.\n        writes.push(() => {\n            const propsToConvert = FONT_PROPERTIES_TO_INHERIT.filter(prop => node.style[prop] === 'inherit');\n            if (propsToConvert.length) {\n                const computedStyle = getComputedStyle(node);\n                for (const prop of propsToConvert) {\n                    node.style.setProperty(prop, computedStyle[prop]);\n                }\n            }\n        });\n    };\n    writes.forEach(fn => fn());\n}\n/**\n * Add styles to all table rows and columns, that are necessary for them to be\n * responsive. This only works if columns have a max-width so the styles are\n * only applied to columns where that is the case.\n *\n * @param {Element} editable\n */\nfunction enforceTablesResponsivity(editable) {\n    // Trying this: https://www.litmus.com/blog/mobile-responsive-email-stacking/\n    const trs = [...editable.querySelectorAll('.o_mail_wrapper tr')]\n        .filter(tr => [...tr.children].some(td => td.classList.contains('o_converted_col')))\n        .reverse();\n    for (const tr of trs) {\n        const commonTable = _createTable();\n        commonTable.style.height = '100%';\n        const commonTr = document.createElement('tr');\n        const commonTd = document.createElement('td');\n        commonTr.appendChild(commonTd);\n        commonTable.appendChild(commonTr);\n        const tds = [...tr.children].filter(child => child.nodeName === 'TD');\n        let index = 0;\n        for (const td of tds) {\n            const width = td.style.maxWidth;\n            const div = document.createElement('div');\n            div.style.display = 'inline-block';\n            div.style.verticalAlign = 'top';\n            div.classList.add('o_stacking_wrapper');\n            commonTd.appendChild(div);\n            const newTable = _createTable();\n            newTable.style.width = width;\n            newTable.classList.add('o_stacking_wrapper');\n            div.appendChild(newTable);\n            const newTr = document.createElement('tr');\n            newTable.appendChild(newTr);\n            newTr.appendChild(td);\n            td.style.width = '100%';\n            td.removeAttribute('width');\n            if (index === 0) {\n                div.before(_createMso(`\n                    <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" role=\"presentation\" style=\"width: 100%;\">\n                        <tr>\n                            <td valign=\"top\" style=\"width: ${width};\">`));\n            } else {\n                div.before(_createMso(`</td><td valign=\"top\" style=\"width: ${width};\">`));\n            }\n            if (index === tds.length - 1) {\n                div.after(_createMso(`</td></tr></table>`));\n            }\n            index++;\n        }\n        const topTd = document.createElement('td');\n        topTd.appendChild(commonTable);\n        tr.prepend(topTd);\n    }\n}\n// Masonry has crazy nested tables that require some extra treatment.\nfunction handleMasonry(editable) {\n    const masonryTrs = editable.querySelectorAll('.s_masonry_block tr');\n    for (const tr of masonryTrs) {\n        const height = _getHeight(tr);\n        const tds = [...tr.children].filter(child => child.nodeName === 'TD');\n        const tdsWithTable = tds.filter(td => [...td.children].some(child => child.nodeName === 'TABLE'));\n        if (tdsWithTable.length) {\n            // TODO: this seems a duplicate of the other o_desktop_h100 set below.\n            // Set the cells' heights to fill their parents.\n            for (const tdWithTable of tdsWithTable) {\n                tdWithTable.classList.add('o_desktop_h100');\n                tdWithTable.style.setProperty('height', '100%');\n            }\n            // We also have to set the same height on the cells' sibling TDs.\n            tds.forEach(td => td.style.setProperty('height', height + 'px'));\n        }\n        // Sometimes Masonry declares rows with a height of 100% but with\n        // columns that overfit the grid. In these cases, we split the rows into\n        // multiple rows so we need to adapt their heights for them to be\n        // divided equally.\n        const trSiblings = [...tr.parentElement.children].filter(child => child.nodeName === 'TR');\n        if (trSiblings.length > 1 && (tr.classList.contains('h-100') || tr.style.getPropertyValue('height') === '100%')) {\n            tr.style.setProperty('height', `${_getHeight(tr.parentElement) / trSiblings.length}px`);\n        }\n    }\n    for (const tr of masonryTrs) {\n        const height = tr.style.height.includes('px') ? parseFloat(tr.style.height.replace('px', '').trim()) : _getHeight(tr);\n        tr.closest('table').classList.add('o_desktop_h100');\n        tr.classList.add('o_desktop_h100');\n        for (const td of [...tr.children].filter(child => child.nodeName === 'TD')) {\n            td.classList.add('o_desktop_h100');\n            td.style.setProperty('height', '100%');\n            const childrenNames = [...td.children].map(child => child.nodeName);\n            if (!childrenNames.includes('TABLE')) {\n                // Hack that makes vertical-align possible within an inline-block.\n                const wrapper = document.createElement('div');\n                wrapper.style.setProperty('display', 'inline-block');\n                wrapper.style.setProperty('width', '100%');\n                // Transfer color to wrapper for Outlook on MacOS/iOS.\n                const tdStyle = getComputedStyle(td);\n                wrapper.style.setProperty('color', tdStyle.color);\n                const firstNonCommentChild = [...td.childNodes].find(child => child.nodeType !== Node.COMMENT_NODE);\n                let anchor;\n                if (firstNonCommentChild) {\n                    anchor = getAdjacentPreviousSiblings(firstNonCommentChild)\n                        .filter(sib => sib.nodeType !== Node.TEXT_NODE)\n                        .shift();\n                }\n                for (const child of [...td.childNodes].filter(child => child.nodeType !== Node.COMMENT_NODE)) {\n                    wrapper.append(child);\n                }\n                anchor ? anchor.after(wrapper) : td.append(wrapper);\n                const centeringSpan = document.createElement('span');\n                centeringSpan.style.setProperty('height', '100%');\n                centeringSpan.style.setProperty('display', 'inline-block');\n                centeringSpan.style.setProperty('vertical-align', 'middle');\n                td.prepend(centeringSpan);\n                // Height on cells should be applied in pixels.\n                if (td.style.height.includes('%')) {\n                    const newHeight = height * parseFloat(td.style.height.replace('%').trim()) / 100;\n                    td.style.setProperty('height', newHeight + 'px');\n                    // Spread height down for responsivity\n                    td.style.setProperty('max-height', newHeight + 'px');\n                    wrapper.style.setProperty('max-height', newHeight + 'px');\n                    if (wrapper.childElementCount === 1 && wrapper.firstElementChild.nodeName === 'IMG' && wrapper.firstElementChild.style.height === '100%') {\n                        wrapper.firstElementChild.style.setProperty('max-height', newHeight + 'px');\n                    }\n                }\n            }\n        }\n    }\n}\n/**\n * Modify the styles of images so they are responsive.\n *\n * @param {Element} editable\n */\nfunction enforceImagesResponsivity(editable) {\n    // Images with 100% height in cells should preserve that height and the\n    // height of the row should be applied to the cell.\n    for (const image of editable.querySelectorAll('td > img')) {\n        const td = image.parentElement;\n        if (td.childElementCount === 1 && (image.classList.contains('h-100') || _getStylePropertyValue(image, 'height') === '100%')) {\n            td.style.setProperty('height', _getHeight(td.parentElement) + 'px');\n            image.style.setProperty('height', '100%');\n        }\n    }\n    // Remove the height attribute in card images so they can resize\n    // responsively, but leave it for Outlook.\n    for (const image of editable.querySelectorAll('img[width=\"100%\"][height]')) {\n        image.before(_createMso(image.outerHTML));\n        image.classList.add('mso-hide');\n        image.removeAttribute('height');\n    }\n}\n/**\n * Convert the contents of an editable area (as a JQuery element) into content\n * that is widely compatible with email clients. If no CSS Rules are given, they\n * will be computed for the editable element's owner document.\n *\n * @param {JQuery} $editable\n * @param {Object} options {$iframe: JQuery;\n *                          wysiwyg: Object}\n */\nexport async function toInline($editable, options) {\n    $editable.removeClass('odoo-editor-editable');\n    const editable = $editable.get(0);\n    const iframe = options.$iframe && options.$iframe.get(0);\n    const wysiwyg = $editable.data('wysiwyg') || options.wysiwyg;\n    const doc = editable.ownerDocument;\n    let cssRules = wysiwyg && wysiwyg._rulesCache;\n    if (!cssRules) {\n        cssRules = getCSSRules(doc);\n        if (wysiwyg) {\n            wysiwyg._rulesCache = cssRules;\n        }\n    }\n\n    // If the editable is not visible, we need to make it visible in order to\n    // retrieve image/icon dimensions. This iterates over ancestors to make them\n    // visible again. We then restore it at the end of this function.\n    const displaysToRestore = [];\n    if (_isHidden(editable)) {\n        let ancestor = editable;\n        while (ancestor && ancestor.nodeName !== 'html' && _isHidden(ancestor)) {\n            if (_getStylePropertyValue(ancestor, 'display') === 'none') {\n                displaysToRestore.push([ancestor, ancestor.style.display]);\n                ancestor.style.setProperty('display', 'block');\n            }\n            ancestor = ancestor.parentElement;\n            if ((!ancestor || ancestor.nodeName === 'HTML') && iframe) {\n                ancestor = iframe;\n            }\n        }\n    }\n    // Fix card-img-top heights (must happen before we transform everything).\n    for (const imgTop of editable.querySelectorAll('.card-img-top')) {\n        imgTop.style.setProperty('height', _getHeight(imgTop) + 'px');\n    }\n\n    attachmentThumbnailToLinkImg($editable);\n    fontToImg($editable);\n    await svgToPng($editable);\n    await webpToPng($editable);\n\n    // Fix img-fluid for Outlook.\n    for (const image of editable.querySelectorAll('img.img-fluid')) {\n        const width = _getWidth(image);\n        const clone = image.cloneNode();\n        clone.setAttribute('width', width);\n        clone.style.setProperty('width', width + 'px');\n        clone.style.removeProperty('max-width');\n        image.before(_createMso(clone.outerHTML));\n        _hideForOutlook(image);\n    }\n\n    classToStyle($editable, cssRules);\n    bootstrapToTable(editable);\n    cardToTable(editable);\n    listGroupToTable(editable);\n    addTables($editable);\n    handleMasonry(editable);\n    const rootFontSizeProperty = getComputedStyle(editable.ownerDocument.documentElement).fontSize;\n    const rootFontSize = parseFloat(rootFontSizeProperty.replace(/[^\\d\\.]/g, ''));\n    normalizeRem($editable, rootFontSize);\n    enforceImagesResponsivity(editable);\n    enforceTablesResponsivity(editable);\n    flattenBackgroundImages(editable);\n    formatTables($editable);\n    normalizeColors($editable);\n    responsiveToStaticForOutlook(editable);\n    // Fix Outlook image rendering bug.\n    for (const attributeName of ['width', 'height']) {\n        const images = editable.querySelectorAll('img');\n        for (const image of images) {\n            if (image.style[attributeName] !== 'auto') {\n                const value = image.getAttribute(attributeName) ||\n                    (attributeName === 'height' && image.offsetHeight) ||\n                    (attributeName === 'width' ? _getWidth(image) : _getHeight(image));\n                if (value) {\n                    image.setAttribute(attributeName, value);\n                    image.style.setProperty(attributeName, value + 'px');\n                }\n            }\n        };\n    };\n    // Fix mx-auto on images in table cells.\n    for (const centeredImage of editable.querySelectorAll('td > img.mx-auto')) {\n        if (centeredImage.parentElement.children.length === 1) {\n            centeredImage.parentElement.style.setProperty('text-align', 'center');\n        }\n    }\n\n    // Remove contenteditable attributes\n    [editable, ...editable.querySelectorAll('[contenteditable]')].forEach(node => node.removeAttribute('contenteditable'));\n\n    // Hide replaced cells on Outlook\n    editable.querySelectorAll('.mso-hide').forEach(_hideForOutlook);\n\n    // Replace double quotes in font-family styles with simple quotes (and\n    // simply remove these styles from images).\n    editable.querySelectorAll('[style*=font-family]').forEach(n => (\n        n.nodeName === 'IMG'\n            ? n.style.removeProperty('font-family')\n            : n.setAttribute('style', n.getAttribute('style').replaceAll('\"', '\\''))\n    ));\n\n    // Styles were applied inline, we don't need a style element anymore.\n    $editable.find('style').remove();\n\n    editable.querySelectorAll('.o_converted_col').forEach(node => node.classList.remove('o_converted_col'));\n\n    for (const [node, displayValue] of displaysToRestore) {\n        node.style.setProperty('display', displayValue);\n    }\n    $editable.addClass('odoo-editor-editable');\n}\n/**\n * Take all elements with a `background-image` style and convert them to `vml`\n * for Outlook. Also remove data-bg-src to avoid Gmail cutting the html.\n *\n * @param {Element} editable\n */\nfunction flattenBackgroundImages(editable) {\n    const backgroundImages = [...editable.querySelectorAll('*[style*=background-image]')]\n        .filter(el => !el.closest('.mso-hide'))\n        .reverse();\n    for (const backgroundImage of backgroundImages) {\n        const vml = _backgroundImageToVml(backgroundImage);\n        if (vml) {\n            // Put the Outlook version after the original one in an mso conditional.\n            backgroundImage.after(_createMso(vml));\n            // Hide the original element for Outlook.\n            backgroundImage.classList.add('mso-hide');\n        }\n        if (backgroundImage.hasAttribute('data-bg-src')) {\n            // Remove data-bg-src as it is not needed for email rendering and\n            // can cause Gmail to cut the email prematurely if the attributes\n            // contain an image in the form of a long base64 string.\n            backgroundImage.removeAttribute('data-bg-src');\n        }\n    }\n}\n/**\n * Convert font icons to images.\n *\n * @param {JQuery} $editable - the element in which the font icons have to be\n *                           converted to images\n */\nfunction fontToImg($editable) {\n    const editable = $editable.get(0);\n    const { fonts } = odoo.loader.modules.get(\"@web_editor/js/wysiwyg/fonts\");\n\n    for (const font of editable.querySelectorAll('.fa')) {\n        let icon, content;\n        fonts.fontIcons.find(fontIcon => {\n            return fonts.getCssSelectors(fontIcon.parser).find(data => {\n                if (font.matches(data.selector.replace(/::?before/g, ''))) {\n                    icon = data.names[0].split('-').shift();\n                    content = data.css.match(/content:\\s*['\"]?(.)['\"]?/)[1];\n                    return true;\n                }\n            });\n        });\n        if (content) {\n            const color = _getStylePropertyValue(font, 'color').replace(/\\s/g, '');\n            let backgroundColoredElement = font;\n            let bg, isTransparent;\n            do {\n                bg = _getStylePropertyValue(backgroundColoredElement, 'background-color').replace(/\\s/g, '');\n                isTransparent = bg === 'transparent' || bg === 'rgba(0,0,0,0)';\n                backgroundColoredElement = backgroundColoredElement.parentElement;\n            } while (isTransparent && backgroundColoredElement);\n            if (bg === 'rgba(0,0,0,0)' && isTransparent) {\n                // default on white rather than black background since opacity\n                // is not supported.\n                bg = 'rgb(255,255,255)';\n            }\n            const style = font.getAttribute('style');\n            const width = _getWidth(font);\n            const height = _getHeight(font);\n            const lineHeight = _getStylePropertyValue(font, 'line-height');\n            // Compute the padding.\n            // First get the dimensions of the icon itself (::before)\n            font.style.setProperty('height', 'fit-content');\n            font.style.setProperty('width', 'fit-content');\n            font.style.setProperty('line-height', 'normal');\n            const intrinsicWidth = _getWidth(font);\n            const intrinsicHeight = _getHeight(font);\n            const hPadding = width && intrinsicWidth && (width - intrinsicWidth) / 2;\n            const vPadding = height && intrinsicHeight && (height - intrinsicHeight) / 2;\n            let padding = '';\n            if (hPadding || vPadding) {\n                padding = vPadding ? vPadding + 'px ' : '0 ';\n                padding += hPadding ? hPadding + 'px' : '0';\n            }\n            const image = document.createElement('img');\n            image.setAttribute('width', intrinsicWidth);\n            image.setAttribute('height', intrinsicHeight);\n            image.setAttribute('src', `/web_editor/font_to_img/${content.charCodeAt(0)}/${encodeURIComponent(color)}/${encodeURIComponent(bg)}/${Math.max(1, Math.round(intrinsicWidth))}x${Math.max(1, Math.round(intrinsicHeight))}`);\n            image.setAttribute('data-class', font.getAttribute('class'));\n            image.setAttribute('data-style', style);\n            image.setAttribute('style', style);\n            image.style.setProperty('box-sizing', 'border-box'); // keep the fontawesome's dimensions\n            image.style.setProperty('line-height', lineHeight);\n            image.style.setProperty('width', intrinsicWidth + 'px');\n            image.style.setProperty('height', intrinsicHeight + 'px');\n            image.style.setProperty('vertical-align', 'unset'); // undo Bootstrap's default (middle).\n            if (!padding) {\n                image.style.setProperty('margin', _getStylePropertyValue(font, 'margin'));\n            }\n            // For rounded images, apply the rounded border to a wrapper, make\n            // sure it doesn't get applied to the image itself so the image\n            // doesn't get cropped in the process.\n            const wrapper = document.createElement('span');\n            wrapper.style.setProperty('display', 'inline-block');\n            wrapper.append(image);\n            font.before(wrapper);\n            if (font.classList.contains('mx-auto')) {\n                wrapper.parentElement.style.textAlign = 'center';\n            }\n            font.remove();\n            wrapper.style.setProperty('padding', padding);\n            const wrapperWidth = width + ['left', 'right'].reduce((sum, side) => (\n                sum + (+_getStylePropertyValue(image, `margin-${side}`).replace('px', '') || 0)\n            ), 0);\n            wrapper.style.setProperty('width', wrapperWidth + 'px');\n            wrapper.style.setProperty('height', height + 'px');\n            wrapper.style.setProperty('vertical-align', 'text-bottom');\n            wrapper.style.setProperty('background-color', image.style.backgroundColor);\n            wrapper.setAttribute('class',\n                'oe_unbreakable ' + // prevent sanitize from grouping image wrappers\n                font.getAttribute('class').replace(new RegExp('(^|\\\\s+)' + icon + '(-[^\\\\s]+)?', 'gi'), '') // remove inline font-awsome style\n            );\n        } else {\n            font.remove();\n        }\n    }\n}\n/**\n * Format table styles so they display well in most mail clients. This implies\n * moving table paddings to its cells, adding tbody (with canceled styles) where\n * needed, and adding pixel heights to parents of elements with percent heights.\n *\n * @param {JQuery} $editable\n */\nfunction formatTables($editable) {\n    const editable = $editable.get(0);\n    const writes = [];\n    for (const table of editable.querySelectorAll('table.o_mail_snippet_general, .o_mail_snippet_general table')) {\n        const tablePaddingTop = parseFloat(_getStylePropertyValue(table, 'padding-top').match(RE_PADDING)[1]);\n        const tablePaddingRight = parseFloat(_getStylePropertyValue(table, 'padding-right').match(RE_PADDING)[1]);\n        const tablePaddingBottom = parseFloat(_getStylePropertyValue(table, 'padding-bottom').match(RE_PADDING)[1]);\n        const tablePaddingLeft = parseFloat(_getStylePropertyValue(table, 'padding-left').match(RE_PADDING)[1]);\n        const rows = [...table.querySelectorAll('tr')].filter(tr => tr.closest('table') === table);\n        const columns = [...table.querySelectorAll('td')].filter(td => td.closest('table') === table);\n        for (const column of columns) {\n            const columnsInRow = [...column.closest('tr').querySelectorAll('td')].filter(td => td.closest('table') === table);\n            const columnIndex = columnsInRow.findIndex(col => col === column);\n            const rowIndex = rows.findIndex(row => row === column.closest('tr'));\n\n            if (!rowIndex) {\n                const match = _getStylePropertyValue(column, 'padding-top').match(RE_PADDING);\n                const columnPaddingTop = match ? parseFloat(match[1]) : 0;\n                writes.push(() => {column.style['padding-top'] = `${columnPaddingTop + tablePaddingTop}px`; });\n            }\n            if (columnIndex === columnsInRow.length - 1) {\n                const match = _getStylePropertyValue(column, 'padding-right').match(RE_PADDING);\n                const columnPaddingRight = match ? parseFloat(match[1]) : 0;\n                writes.push(() => {column.style['padding-right'] = `${columnPaddingRight + tablePaddingRight}px`; });\n            }\n            if (rowIndex === rows.length - 1) {\n                const match = _getStylePropertyValue(column, 'padding-bottom').match(RE_PADDING);\n                const columnPaddingBottom = match ? parseFloat(match[1]) : 0;\n                writes.push(() => {column.style['padding-bottom'] = `${columnPaddingBottom + tablePaddingBottom}px`; });\n            }\n            if (!columnIndex) {\n                const match = _getStylePropertyValue(column, 'padding-left').match(RE_PADDING);\n                const columnPaddingLeft = match ? parseFloat(match[1]) : 0;\n                writes.push(() => {column.style['padding-left'] = `${columnPaddingLeft + tablePaddingLeft}px`; });\n            }\n        }\n        writes.push(() => { table.style.removeProperty('padding'); });\n    }\n    writes.forEach((fn) => fn());\n    // Ensure a tbody in every table and cancel its default style.\n    for (const table of [...editable.querySelectorAll('table')].filter(n => ![...n.children].some(c => c.nodeName === 'TBODY'))) {\n        const contents = [...table.childNodes];\n        const tbody = document.createElement('tbody');\n        tbody.style.setProperty('vertical-align', 'top');\n        table.prepend(tbody);\n        tbody.append(...contents);\n    }\n    // Children will only take 100% height if the parent has a height property.\n    for (const node of [...editable.querySelectorAll('*')].filter(n => (\n        n.style && n.style.getPropertyValue('height') === '100%' && (\n            !n.parentElement.style.getPropertyValue('height') ||\n            n.parentElement.style.getPropertyValue('height').includes('%'))\n    ))) {\n        let parent = node.parentElement;\n        let height = parent.style.getPropertyValue('height');\n        while (parent && height && height.includes('%')) {\n            parent = parent.parentElement;\n            height = parent.style.getPropertyValue('height');\n        }\n        if (parent) {\n            parent.style.setProperty('height', $(parent).height());\n        }\n    }\n    // Align self and justify content don't work on table cells.\n    for (const cell of editable.querySelectorAll('td')) {\n        const alignSelf = cell.style.alignSelf;\n        const justifyContent = cell.style.justifyContent;\n        if (alignSelf === 'start' || justifyContent === 'start' || justifyContent === 'flex-start') {\n            cell.style.verticalAlign = 'top';\n        } else if (alignSelf === 'center' || justifyContent === 'center') {\n            const parentCell = cell.parentElement.closest('td');\n            const parentTable = cell.closest('table');\n            if (parentCell) {\n                parentTable.style.height = _getHeight(parentCell) + 'px';\n            }\n            cell.style.verticalAlign = 'middle';\n        } else if (alignSelf === 'end' || justifyContent === 'end' || justifyContent === 'flex-end') {\n            cell.style.verticalAlign = 'bottom';\n        }\n    }\n    // Align items doesn't work on table rows.\n    for (const row of editable.querySelectorAll('tr')) {\n        const alignItems = row.style.alignItems;\n        if (alignItems === 'flex-start') {\n            row.style.verticalAlign = 'top';\n        } else if (alignItems === 'center') {\n            row.style.verticalAlign = 'middle';\n        } else if (alignItems === 'flex-end' || alignItems === 'baseline') {\n            row.style.verticalAlign = 'bottom';\n        } else if (alignItems === 'stretch') {\n            const columns = [...row.querySelectorAll('td.o_converted_col')];\n            if (columns.length > 1) {\n                const commonAncestor = commonParentGet(columns[0], columns[1]);\n                const biggestHeight = commonAncestor.clientHeight;\n                for (const column of columns) {\n                    column.style.height = biggestHeight + 'px';\n                }\n            }\n        }\n    }\n    // Tables don't properly inherit certain styles from their ancestors in Outlook.\n    for (const table of editable.querySelectorAll('table')) {\n        const propsToConvert = FONT_PROPERTIES_TO_INHERIT.filter(prop => table.style[prop] === 'inherit' || !table.style[prop]);\n        if (propsToConvert.length) {\n            for (const prop of propsToConvert) {\n                let ancestor = table;\n                while (ancestor && (!ancestor.style[prop] || ancestor.style[prop] === 'inherit')) {\n                    ancestor = ancestor.parentElement;\n                }\n                if (ancestor) {\n                    table.style.setProperty(prop, ancestor.style[prop]);\n                }\n            }\n        }\n    }\n}\n/**\n * Parse through the given document's stylesheets, preprocess(*) them and return\n * the result as an array of objects, each containing a selector string , a\n * style object and a specificity number. Preprocessing involves grouping\n * whatever rules can be grouped together and precomputing their specificity so\n * as to sort them appropriately.\n *\n * @param {Document} doc\n * @returns {Object[]} Array<{selector: string;\n *                            style: {[styleName]: string};\n *                            specificity: number;}>\n */\nexport function getCSSRules(doc) {\n    const cssRules = [];\n    for (const sheet of doc.styleSheets) {\n        // try...catch because browser may not able to enumerate rules for cross-domain sheets\n        let rules;\n        try {\n            rules = sheet.rules || sheet.cssRules;\n        } catch (e) {\n            console.log(\"Can't read the css rules of: \" + sheet.href, e);\n            continue;\n        }\n        for (const rule of (rules || [])) {\n            const subRules = [rule];\n            const conditionText = rule.conditionText;\n            const minWidthMatch = conditionText && conditionText.match(/\\(min-width *: *(\\d+)/);\n            const minWidth = minWidthMatch && +(minWidthMatch[1] || '0');\n            if (minWidth && minWidth >= 992) {\n                // Large min-width media queries should be included.\n                // eg., .container has a default max-width for all screens.\n                let mediaRules;\n                try {\n                    mediaRules = rule.rules || rule.cssRules;\n                    subRules.push(...mediaRules);\n                } catch (e) {\n                    console.log(`Can't read the css rules of: ${sheet.href} (${conditionText})`, e);\n                }\n            }\n            for (const subRule of subRules) {\n                const selectorText = subRule.selectorText || '';\n                // Split selectors, making sure not to split at commas in parentheses.\n                for (const selector of selectorText.split(RE_COMMAS_OUTSIDE_PARENTHESES)) {\n                    if (selector && !SELECTORS_IGNORE.test(selector)) {\n                        cssRules.push({ selector: selector.trim(), rawRule: subRule });\n                        if (selector === 'body') {\n                            // The top element of a mailing has the class\n                            // 'o_layout'. Give it the body's styles so they can\n                            // trickle down.\n                            cssRules.push({ selector: '.o_layout', rawRule: subRule, specificity: 1 });\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    return cssRules;\n}\n/**\n * Convert Bootstrap list groups and their items to table structures.\n *\n * @param {Element} editable\n */\nfunction listGroupToTable(editable) {\n    for (const listGroup of editable.querySelectorAll('.list-group')) {\n        let table;\n        if (listGroup.querySelectorAll('.list-group-item').length) {\n            table = _createTable(listGroup.attributes);\n        } else {\n            table = listGroup.cloneNode();\n            for (const attr of listGroup.attributes) {\n                table.setAttribute(attr.name, attr.value);\n            }\n        }\n        for (const child of [...listGroup.childNodes]) {\n            if (child.classList && child.classList.contains('list-group-item')) {\n                // List groups are <ul>s that render like tables. Their\n                // li.list-group-item children should translate to tr > td.\n                const row = document.createElement('tr');\n                const col = document.createElement('td');\n                for (const attr of child.attributes) {\n                    col.setAttribute(attr.name, attr.value);\n                }\n                col.append(...child.childNodes);\n                col.classList.remove('list-group-item');\n                if (!col.className) {\n                    col.removeAttribute('class');\n                }\n                row.append(col);\n                table.append(row);\n                child.remove();\n            } else if (child.nodeName === 'LI') {\n                table.append(...child.childNodes);\n            } else {\n                table.append(child);\n            }\n        }\n        table.classList.remove('list-group');\n        if (!table.className) {\n            table.removeAttribute('class');\n        }\n        if (listGroup.nodeName === 'TD') {\n            listGroup.append(table);\n            listGroup.classList.remove('list-group');\n            if (!listGroup.className) {\n                listGroup.removeAttribute('class');\n            }\n        } else {\n            listGroup.before(table);\n            listGroup.remove();\n        }\n    }\n}\n/**\n * Convert all styles containing rgb colors to hexadecimal colors.\n * Note: ignores rgba colors, which are not supported in Microsoft Outlook.\n *\n * @param {JQuery} $editable\n */\nfunction normalizeColors($editable) {\n    const editable = $editable.get(0);\n    for (const node of editable.querySelectorAll('[style*=\"rgb\"]')) {\n        const rgbaMatch = node.getAttribute('style').match(/rgba?\\(([\\d\\.]+\\s*,?\\s*){3,4}\\)/g);\n        for (const rgb of rgbaMatch || []) {\n            node.setAttribute('style', node.getAttribute('style').replace(rgb, rgbToHex(rgb, node)));\n        }\n    }\n}\n/**\n * Convert all css values that use the rem unit to px.\n *\n * @param {JQuery} $editable\n * @param {Number} rootFontSize=16 The font size of the root element, in pixels\n */\nfunction normalizeRem($editable, rootFontSize=16) {\n    const editable = $editable.get(0);\n    for (const node of editable.querySelectorAll('[style*=\"rem\"]')) {\n        const remMatch = node.getAttribute('style').match(/[\\d\\.]+\\s*rem/g);\n        for (const rem of remMatch || []) {\n            const remValue = parseFloat(rem.replace(/[^\\d\\.]/g, ''));\n            const pxValue = Math.round(remValue * rootFontSize * 100) / 100;\n            node.setAttribute('style', node.getAttribute('style').replace(rem, pxValue + 'px'));\n        }\n    }\n}\n\n/**\n * This replaces column html with a dumbed down, Outlook-compliant version of\n * them just for Outlook so while not responsive, these columns still display OK\n * on Outlook.\n *\n * @param {Element} editable\n */\n function responsiveToStaticForOutlook(editable) {\n    // Replace the responsive tables with static ones for Outlook\n    for (const td of editable.querySelectorAll('td.o_converted_col:not(.mso-hide)')) {\n        const tdStyle = td.getAttribute('style') || '';\n        const msoAttributes = [...td.attributes].filter(attr => attr.name !== 'style' && attr.name !== 'width');\n        const msoWidth = td.style.getPropertyValue('max-width');\n        const msoStyles = tdStyle.replace(/(^| |max-)width:[^;]*;\\s*/g, '');\n        const outlookTd = document.createElement('td');\n        for (const attribute of msoAttributes) {\n            outlookTd.setAttribute(attribute.name, td.getAttribute(attribute.name));\n        }\n        if (msoWidth) {\n            outlookTd.setAttribute('width', ('' + msoWidth).replace('px', '').trim());\n            outlookTd.setAttribute('style', `${msoStyles}width: ${msoWidth};`);\n        } else {\n            outlookTd.setAttribute('style', msoStyles);\n        }\n        if (td.closest('.s_masonry_block')) {\n            outlookTd.style.padding = 0; // Not sure why this is needed.\n        }\n        // Outlook doesn't support left/right padding on images. When the image\n        // is the only child of its parent, apply said padding to the parent.\n        if (td.children.length === 1 && td.firstElementChild.nodeName === 'IMG') {\n            const tdComputedStyle = getComputedStyle(td);\n            for (const side of ['left', 'right']) {\n                if (td.firstElementChild.style.width === '100%') {\n                    const prop = `padding-${side}`;\n                    const imagePadding = +td.firstElementChild.style[prop].replace('px', '');\n                    if (imagePadding > 0) {\n                        const tdPadding = +tdComputedStyle[prop].replace('px', '') || 0;\n                        outlookTd.style[prop] = tdPadding + imagePadding + 'px';\n                    }\n                }\n            }\n        }\n        // The opening tag of `outlookTd` is for Outlook.\n        td.before(_createMso(outlookTd.outerHTML.replace('</td>', '')));\n        // The opening tag of `td` is for the others.\n        _hideForOutlook(td, 'opening');\n    }\n}\n\n/**\n * Convert image element to an image element with type png\n *\n * @param {img} HTMLElement\n */\n\nasync function convertToPng(img) {\n    // Make sure the image is loaded before we convert it.\n    await new Promise(resolve => {\n        img.onload = () => resolve();\n        if (img.complete) {\n            resolve();\n        }\n    });\n    const image = document.createElement('img');\n    const canvas = document.createElement('CANVAS');\n    const width = _getWidth(img);\n    const height = _getHeight(img);\n\n    canvas.setAttribute('width', width);\n    canvas.setAttribute('height', height);\n    canvas.getContext('2d').drawImage(img, 0, 0, width, height);\n\n    for (const attribute of img.attributes) {\n        image.setAttribute(attribute.name, attribute.value);\n    }\n\n    image.setAttribute('src', canvas.toDataURL('png'));\n    image.setAttribute('width', width);\n    image.setAttribute('height', height);\n    return image;\n}\n\n/**\n * Convert images of type svg to png.\n *\n * @param {JQuery} $editable\n */\nasync function svgToPng($editable) {\n    for (const svg of $editable.find('img[src*=\".svg\"]')) {\n        const image = await convertToPng(svg);\n        svg.before(image);\n        svg.remove();\n    }\n}\n\n/**\n * Convert images of type webp to png.\n *\n * @param {JQuery} $editable\n */\nasync function webpToPng($editable) {\n    for (const webp of $editable.find('img[src*=\".webp\"]')) {\n        const image = await convertToPng(webp);\n        webp.before(image);\n        webp.remove();\n    }\n\n    for (const webp of $editable.find('[style*=\"background-image\"][style*=\".webp\"]')) {\n        // Create an image element with the background image and replace the url\n        // with the png converted image url\n        const width = _getWidth(webp);\n        const height = _getHeight(webp);\n        const tempImage = document.createElement(\"img\");\n        tempImage.setAttribute(\"src\", webp.style.backgroundImage.slice(5, -2));\n        tempImage.setAttribute(\"width\", width);\n        tempImage.setAttribute(\"height\", height);\n        webp.before(tempImage);\n        const image = await convertToPng(tempImage);\n        webp.style.backgroundImage = `url(${image.getAttribute(\"src\")})`;\n        tempImage.remove();\n    }\n}\n\n//--------------------------------------------------------------------------\n// Private\n//--------------------------------------------------------------------------\n\n/**\n * Take an element and apply a colspan to it. In this context, this implies to\n * also apply a width to it, that corresponds to the colspan.\n *\n * @param {Element} element\n * @param {number} colspan\n * @param {number} tableWidth\n */\nfunction _applyColspan(element, colspan, tableWidth) {\n    element.setAttribute('colspan', colspan);\n    const widthPercentage = +element.getAttribute('colspan') / 12;\n    // Round to 2 decimal places.\n    const width = Math.round(tableWidth * widthPercentage * 100) / 100;\n    element.style.setProperty('max-width', width + 'px');\n    element.classList.add('o_converted_col');\n}\n/**\n * Take an element with a background image and return a string containing the\n * VML code to display the same image properly in Outlook, with its contents\n * inside.\n * Note that this assumes:\n *   - background-size: cover,\n *   - background-repeat: no-repeat,\n *   - size 100%\n *   - content is centered x/y\n * TODO: centering span probably not needed with `v-text-anchor:middle` present.\n *\n * @param {Element} backgroundImage\n * @returns {string}\n */\nfunction _backgroundImageToVml(backgroundImage) {\n    const matches = backgroundImage.style.backgroundImage.match(/url\\(\"?(.+?)\"?\\)/);\n    const url = matches && matches[1];\n    if (url) {\n        // Create the outer structure.\n        const clone = backgroundImage.cloneNode(true);\n        const div = document.createElement('div');\n        div.replaceChildren(...clone.childNodes);\n        [['fontSize', 0], ['height', '100%'], ['width', '100%']].forEach(([k, v]) => div.style[k] = v);\n        const vmlContent = document.createElement('div');\n        vmlContent.append(div);\n\n        // Preserve important inherited properties without ancestor context.\n        const style = getComputedStyle(backgroundImage);\n        for (const prop of FONT_PROPERTIES_TO_INHERIT) {\n            div.style[prop] = backgroundImage.style[prop] || style[prop];\n        }\n        [...div.children].forEach(child => child.style.setProperty('font-size', child.style.fontSize || style.fontSize));\n\n        // Prepare the top element for hosting the VML image.\n        for (const prop of ['background', 'background-image', 'background-repeat', 'background-size']) {\n            clone.style.removeProperty(prop);\n        }\n        clone.style.padding = 0;\n        clone.className = clone.className.replace(/p[bt]\\d+/g, ''); // Remove padding classes.\n        clone.setAttribute('background', url);\n        clone.setAttribute('valign', 'middle');\n\n        // Create the VML structure, with the content of the original element inside.\n        const [width, height] = [_getWidth(backgroundImage), _getHeight(backgroundImage)];\n        const vml = `<v:image xmlns:v=\"urn:schemas-microsoft-com:vml\" fill=\"true\" stroke=\"false\" ` +\n            `style=\"border: 0; display: inline-block; width: ${width}px; height: ${height}px;\" src=\"${url}\"/>\n        <v:rect xmlns:v=\"urn:schemas-microsoft-com:vml\" fill=\"true\" stroke=\"false\" ` +\n            `style=\"border: 0; display: inline-block; position: absolute; width:${width}px; height:${height}px; v-text-anchor:middle;\">\n            <v:fill opacity=\"0%\" color=\"#000000\"/>\n            <v:textbox inset=\"0,0,0,0\">\n                <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\">\n                    <tr>\n                        <td width=\"${width}\" align=\"center\" style=\"text-align: center;\">${vmlContent.outerHTML}</td>\n                    </tr>\n                </table>\n            </v:textbox>\n        </v:rect>`;\n\n        // Wrap the VML in the original opening and closing tags.\n        return `${clone.outerHTML.replace(/<\\/[\\w-]+>[\\s\\n]*$/, '')}${vml}</${clone.nodeName.toLowerCase()}>`;\n    }\n}\n/**\n * Take a selector and return its specificity according to the w3 specification.\n *\n * @see http://www.w3.org/TR/css3-selectors/#specificity\n * @param {string} selector\n * @returns number\n */\nfunction _computeSpecificity(selector) {\n    let a = 0;\n    selector = selector.replace(/#[a-z0-9_-]+/gi, () => { a++; return ''; });\n    let b = 0;\n    selector = selector.replace(/(\\.[a-z0-9_-]+)|(\\[.*?\\])/gi, () => { b++; return ''; });\n    let c = 0;\n    selector = selector.replace(/(^|\\s+|:+)[a-z0-9_-]+/gi, a => { if (!a.includes(':not(')) c++; return ''; });\n    return (a * 100) + (b * 10) + c;\n}\n/**\n * Take all the rules and modify them to contain information on their\n * specificity and to have normalized style.\n *\n * @see _computeSpecificity\n * @see _normalizeStyle\n * @param {Object} cssRules\n */\nfunction _computeStyleAndSpecificityOnRules(cssRules) {\n    for (const cssRule of cssRules) {\n        if (!cssRule.style && cssRule.rawRule.style) {\n            const style = _normalizeStyle(cssRule.rawRule.style);\n            if (Object.keys(style).length) {\n                Object.assign(cssRule,  { style, specificity: _computeSpecificity(cssRule.selector) });\n            }\n        }\n    }\n}\n/**\n * Return an array of twelve table cells as JQuery elements.\n *\n * @returns {Element[]}\n */\nfunction _createColumnGrid() {\n    return new Array(12).fill().map(() => document.createElement('td'));\n}\n/**\n * Return a comment element with the given content, wrapped in an mso condition.\n *\n * @param {string} content\n * @returns {Comment}\n */\nfunction _createMso(content = \"\") {\n    // We remove commets having opposite condition fron the one we will insert\n    // We remove comment tags having the same condition\n    const showRegex = /<!--\\[if\\s+mso\\]>([\\s\\S]*?)<!\\[endif\\]-->/g;\n    const hideRegex = /<!--\\[if\\s+!mso\\]>([\\s\\S]*?)<!\\[endif\\]-->/g;\n    let contentToInsert = content;\n    contentToInsert = contentToInsert.replace(showRegex, (matchedContent, group) => group);\n    contentToInsert = contentToInsert.replace(hideRegex, \"\");\n    return document.createComment(`[if mso]>${contentToInsert}<![endif]`);\n}\n/**\n * Return a table element, with its default styles and attributes, as well as\n * the applicable given attributes, if any.\n *\n * @see TABLE_ATTRIBUTES\n * @see TABLE_STYLES\n * @param {NamedNodeMap | Attr[]} [attributes] default: []\n * @returns {Element}\n */\nfunction _createTable(attributes = []) {\n    const table = document.createElement('table');\n    Object.entries(TABLE_ATTRIBUTES).forEach(([att, value]) => table.setAttribute(att, value));\n    for (const attr of attributes) {\n        if (!(attr.name === 'width' && attr.value === '100%')) {\n            table.setAttribute(attr.name, attr.value);\n        }\n    }\n    table.style.setProperty('width', '100%', 'important');\n    if (table.classList.contains('o_layout')) {\n        // The top mailing element inherits the body's font size and line-height\n        // and should keep them.\n        const layoutStyles = {...TABLE_STYLES};\n        delete layoutStyles['font-size'];\n        delete layoutStyles['line-height'];\n        Object.entries(layoutStyles).forEach(([att, value]) => table.style[att] = value)\n    } else {\n        for (const styleName in TABLE_STYLES) {\n            if (!('style' in attributes && attributes.style.value.includes(styleName + ':'))) {\n                table.style[styleName] = TABLE_STYLES[styleName];\n            }\n        }\n    }\n    return table;\n}\n/**\n * Take a Bootstrap grid column element and return its size, computed by using\n * its Bootstrap classes.\n *\n * @see RE_COL_MATCH\n * @param {Element} column\n * @returns {number}\n */\nfunction _getColumnSize(column) {\n    const colMatch = column.className.match(RE_COL_MATCH);\n    const colOptions = colMatch[2] && colMatch[2].substr(1).split('-');\n    const colSize = colOptions && (colOptions.length === 2 ? +colOptions[1] : +colOptions[0]) || 0;\n    return colSize;\n}\n/**\n * Take a Bootstrap grid column element and return its offset size, computed by\n * using its Bootstrap classes.\n *\n * @see RE_OFFSET_MATCH\n * @param {Element} column\n * @returns {number}\n */\nfunction _getColumnOffsetSize(column) {\n    const offsetMatch = column.className.match(RE_OFFSET_MATCH);\n    const offsetOptions = offsetMatch && offsetMatch[2] && offsetMatch[2].substr(1).split('-');\n    const offsetSize = offsetOptions && (offsetOptions.length === 2 ? +offsetOptions[1] : +offsetOptions[0]) || 0;\n    return offsetSize;\n}\n\n\nfunction isBlacklistedStyle(node, selector, key) {\n    return (\n        node.matches(\"table, thead, tbody, tfoot, tr, td, th\") &&\n        [\"table\", \"thead\", \"tbody\", \"tfoot\", \"tr\", \"td\", \"th\"].some((elName) => selector.includes(elName)) &&\n        key.includes(\"color\")\n    );\n}\n\nfunction removeBlacklistedStyles(rule, node, checkBlacklisted) {\n    if (!checkBlacklisted || !rule.style) {\n        return rule.style;\n    }\n    const styles = {};\n    for (const [key, value] of Object.entries(rule.style)) {\n        if (isBlacklistedStyle(node, rule.selector, key)) {\n            continue;\n        }\n        styles[key] = value;\n    }\n    return styles;\n}\n/**\n * Return the CSS rules which applies on an element, tweaked so that they are\n * browser/mail client ok.\n *\n * @param {Node} node\n * @param {Object[]} Array<{selector: string;\n *                          style: {[styleName]: string};\n *                          specificity: number;}>\n * @returns {Object} {[styleName]: string}\n */\nfunction _getMatchedCSSRules(node, cssRules, checkBlacklisted = false) {\n    node.matches = node.matches || node.webkitMatchesSelector || node.mozMatchesSelector || node.msMatchesSelector || node.oMatchesSelector;\n    const styles = cssRules\n        .map((rule) => removeBlacklistedStyles(rule, node, checkBlacklisted))\n        .filter(Boolean);\n\n    // Add inline styles at the highest specificity.\n    if (node.style.length) {\n        const inlineStyles = {};\n        for (const styleName of node.style) {\n            inlineStyles[styleName] = node.style[styleName];\n        }\n        styles.push(inlineStyles);\n    }\n\n    const processedStyle = {};\n    for (const style of styles) {\n        for (const [key, value] of Object.entries(style)) {\n            if (!processedStyle[key] || !processedStyle[key].includes('important') || value.includes('important')) {\n                processedStyle[key] = value;\n            }\n        }\n    }\n\n    for (const [key, value] of Object.entries(processedStyle)) {\n        if (value && value.endsWith('important')) {\n            processedStyle[key] = value.replace(/\\s*!important\\s*$/, '');\n        }\n    };\n    // In case the groupStyle have var in its value, the substyles will not have\n    // any value assigned in cssRules thus we loose the style since the groupStyle\n    // doesn't appear too in cssRules. As a solution we added those substyle using\n    // their computed values\n    const computedStyle = getComputedStyle(node);\n    for (const groupName in GROUPED_STYLES) {\n        // We exclude the 'margin' and 'padding' styles from force apply because\n        // it's common that they have a value set by auto which doesn't make sense to\n        // force their computed value.\n        const force = !groupName.includes(\"margin\") && !groupName.includes(\"padding\");\n        const hasSubStyleApplied = GROUPED_STYLES[groupName].some(\n            (styleName) => styleName in processedStyle\n        );\n        if (!force && hasSubStyleApplied) {\n            continue;\n        }\n        for (const styleName of GROUPED_STYLES[groupName]) {\n            const styleValue = computedStyle.getPropertyValue(styleName);\n            if (styleValue && typeof styleValue === \"string\" && styleValue.length) {\n                processedStyle[styleName] = styleValue;\n            }\n        }\n    }\n\n    if (processedStyle.display === 'block' && !(node.classList && node.classList.contains('oe-nested'))) {\n        delete processedStyle.display;\n    }\n    if (!processedStyle['box-sizing']) {\n        processedStyle['box-sizing'] = 'border-box'; // This is by default with Bootstrap.\n    }\n\n    // The css generates all the attributes separately and not in simplified\n    // form. In order to have a better compatibility (outlook for example) we\n    // simplify the css tags. e.g. border-left-style: none; border-bottom-s ....\n    // will be simplified in border-style = none\n    for (const info of [\n        {name: 'margin'},\n        {name: 'padding'},\n        {name: 'border', suffix: '-style', defaultValue: 'none'},\n    ]) {\n        const positions = ['top', 'right', 'bottom', 'left'];\n        const positionalKeys = positions.map(position => `${info.name}-${position}${info.suffix || ''}`);\n        const styles = positionalKeys.map(key => processedStyle[key]).filter(s => s);\n        const hasVariableStyle = styles.some(style => style.includes('calc(') || style.includes('var('));\n        const inherits = positionalKeys.some(key => ['inherit', 'initial'].includes((processedStyle[key] || '').trim()));\n        if (styles.length && !hasVariableStyle && !inherits) {\n            const propertyName = `${info.name}${info.suffix || ''}`;\n            processedStyle[propertyName] = positionalKeys.every(key => processedStyle[positionalKeys[0]] === processedStyle[key])\n                ? processedStyle[propertyName] = processedStyle[positionalKeys[0]] // top = right = bottom = left => property: [top];\n                : positionalKeys.map(key => processedStyle[key] || (info.defaultValue || 0)).join(' '); // property: [top] [right] [bottom] [left];\n            for (const prop of positionalKeys) {\n                delete processedStyle[prop];\n            }\n        }\n    };\n\n    if (processedStyle['border-bottom-left-radius']) {\n        processedStyle['border-radius'] = processedStyle['border-bottom-left-radius'];\n        delete processedStyle['border-bottom-left-radius'];\n        delete processedStyle['border-bottom-right-radius'];\n        delete processedStyle['border-top-left-radius'];\n        delete processedStyle['border-top-right-radius'];\n    }\n\n    // If the border styling is initial we remove it to simplify the css tags\n    // for compatibility. Also, since we do not send a css style tag, the\n    // initial value of the border is useless.\n    for (const styleName in processedStyle) {\n        if (styleName.includes('border') && processedStyle[styleName] === 'initial') {\n            delete processedStyle[styleName];\n        }\n    };\n\n    // text-decoration rule is decomposed in -line, -color and -style. This is\n    // however not supported by many browser/mail clients and the editor does\n    // not allow to change -color and -style rule anyway\n    if (processedStyle['text-decoration-line']) {\n        processedStyle['text-decoration'] = processedStyle['text-decoration-line'];\n        delete processedStyle['text-decoration-line'];\n        delete processedStyle['text-decoration-color'];\n        delete processedStyle['text-decoration-style'];\n        delete processedStyle['text-decoration-thickness'];\n    }\n\n    // flexboxes are not supported in Windows Outlook\n    for (const styleName in processedStyle) {\n        if (styleName.includes('flex') || `${processedStyle[styleName]}`.includes('flex')) {\n            delete processedStyle[styleName];\n        }\n    }\n\n    return processedStyle;\n}\nlet lastComputedStyleElement;\nlet lastComputedStyle\n/**\n * Return the value of the given style property on the given element. This\n * caches the last computed style so if it's called several times in a row for\n * the same element, we don't recompute it every time.\n *\n * @param {Element} element\n * @param {string} propertyName\n * @returns\n */\nfunction _getStylePropertyValue(element, propertyName) {\n    const computedStyle = lastComputedStyleElement === element ? lastComputedStyle : getComputedStyle(element)\n    lastComputedStyleElement = element;\n    lastComputedStyle = computedStyle;\n    return computedStyle[propertyName] || element.style.getPropertyValue(propertyName);\n}\n/**\n * Equivalent to JQuery's `width` method. Returns the element's visible width.\n *\n * @param {Element} element\n * @returns {Number}\n */\nfunction _getWidth(element) {\n    return parseFloat(getComputedStyle(element).width.replace('px', '')) || 0;\n}\n/**\n * Equivalent to JQuery's `height` method. Returns the element's visible height.\n *\n * @param {Element} element\n * @returns {Number}\n */\nfunction _getHeight(element) {\n    return parseFloat(getComputedStyle(element).height.replace('px', '')) || 0;\n}\n/**\n * Hides the given node (or just its opening/closing tag) for Outlook with mso\n * conditional comments and, if needed, mso hide style.\n *\n * @param {Node} node\n * @param {false|'opening'|'closing'} [onlyHideTag=false]\n */\nfunction _hideForOutlook(node, onlyHideTag = false) {\n    if (!onlyHideTag) {\n        node.setAttribute('style', `${node.getAttribute('style') || ''} mso-hide: all;`.trim());\n    }\n    node[onlyHideTag === 'closing' ? 'append' : 'before'](document.createComment('[if !mso]><!'));\n    node[onlyHideTag === 'opening' ? 'prepend' : 'after'](document.createComment('<![endif]'));\n}\n/**\n * Return true if the given element is hidden.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent\n * @param {Element} element\n * @returns {boolean}\n */\nfunction _isHidden(element) {\n    return element.offsetParent === null;\n}\n/**\n * Take a css style declaration return a \"normalized\" version of it (as a\n * standard object) for the purposes of emails. This means removing its styles\n * that are invalid, describe animations or aren't standard css (webkit\n * extensions). It also involves adding the \"!important\" suffix to styles that\n * have that priority, so they can be handled without access to the full\n * declaration.\n *\n * @param {CSSStyleDeclaration} style\n * @returns {Object} {[styleName]: string}\n */\nfunction _normalizeStyle(style) {\n    const normalizedStyle = {};\n    for (const styleName of style) {\n        const value = style[styleName];\n        if (\n            value &&\n            !styleName.includes(\"animation\") &&\n            !styleName.includes(\"-webkit\") &&\n            typeof value === \"string\"\n        ) {\n            const normalizedStyleName = styleName.replace(/-(.)/g, (a, b) => b.toUpperCase());\n            normalizedStyle[styleName] = style[normalizedStyleName];\n            if (style.getPropertyPriority(styleName) === 'important') {\n                normalizedStyle[styleName] += ' !important';\n            }\n        }\n    }\n    return normalizedStyle;\n}\n/**\n * Wrap a given element into a new parent, in place.\n *\n * @param {Element} element\n * @param {string} wrapperTag\n * @param {string} [wrapperClass] optional class to apply to the wrapper\n * @param {string} [wrapperStyle] optional style to apply to the wrapper\n * @returns {Element} the wrapper\n */\n function _wrap(element, wrapperTag, wrapperClass, wrapperStyle) {\n    const wrapper = document.createElement(wrapperTag);\n    if (wrapperClass) {\n        wrapper.className = wrapperClass;\n    }\n    if (wrapperStyle) {\n        wrapper.style.cssText = wrapperStyle;\n    }\n    element.parentElement.insertBefore(wrapper, element);\n    wrapper.append(element);\n    return wrapper;\n}\n\n/**\n * Corrects the `border-style` attribute in the provided inline style string.\n * This is specifically for Outlook, which displays borders even when their widths are set to 0px.\n * If all border widths are 0, the function updates `border-style` to `none`.\n *\n * @param {string} style - The inline style string to correct.\n * @returns {string} - The corrected inline style string.\n */\nfunction correctBorderAttributes(style) {\n    const stylesObject = style\n        .replace(/\\s+/g, \" \")\n        .split(\";\")\n        .reduce((styles, styleString) => {\n            const [attribute, value] = styleString.split(\":\").map((str) => str.trim());\n            if (attribute) {\n                styles[attribute] = value;\n            }\n            return styles;\n        }, {});\n\n    const BORDER_WIDTHS_ATTRIBUTES = [\n        \"border-bottom-width\",\n        \"border-left-width\",\n        \"border-right-width\",\n        \"border-top-width\",\n    ];\n\n    const isBorderStyleApplied = BORDER_WIDTHS_ATTRIBUTES.some(\n        (attribute) => attribute in stylesObject\n    );\n\n    if (!isBorderStyleApplied) {\n        return style;\n    }\n\n    const totalBorderWidth = BORDER_WIDTHS_ATTRIBUTES.reduce((totalWidth, attribute) => {\n        const widthValue = stylesObject[attribute] || \"0px\";\n        const numericWidth = parseFloat(widthValue.replace(\"px\", \"\")) || 0;\n        return totalWidth + numericWidth;\n    }, 0);\n\n    if (totalBorderWidth === 0) {\n        let correctedStyle = style.trim();\n        if (correctedStyle.slice(-1) != ';') {\n            correctedStyle += ';';\n        }\n        correctedStyle = correctedStyle.replace(\n            /(;|^)\\s*border-style\\s*:[^;]*(;|$)|$/, '$1border-style:none$2'\n        );\n        return correctedStyle;\n    }\n\n    return style;\n}\n\nexport default {\n    addTables: addTables,\n    attachmentThumbnailToLinkImg: attachmentThumbnailToLinkImg,\n    bootstrapToTable: bootstrapToTable,\n    cardToTable: cardToTable,\n    classToStyle: classToStyle,\n    fontToImg: fontToImg,\n    formatTables: formatTables,\n    getCSSRules: getCSSRules,\n    listGroupToTable: listGroupToTable,\n    normalizeColors: normalizeColors,\n    normalizeRem: normalizeRem,\n    toInline: toInline,\n    createMso: _createMso,\n};\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { QWebPlugin } from '@web_editor/js/backend/QWebPlugin';\nimport { TranslationButton } from \"@web/views/fields/translation_button\";\nimport { useDynamicPlaceholder } from \"@web/views/fields/dynamic_placeholder_hook\";\nimport { useBus, useSpellCheck } from \"@web/core/utils/hooks\";\nimport {\n    getAdjacentPreviousSiblings,\n    getAdjacentNextSiblings,\n    getRangePosition\n} from '@web_editor/js/editor/odoo-editor/src/utils/utils';\nimport { toInline } from '@web_editor/js/backend/convert_inline';\nimport { getBundle, loadBundle } from '@web/core/assets';\nimport { ensureJQuery } from '@web/core/ensure_jquery';\nimport {\n    Component,\n    useRef,\n    useState,\n    onWillStart,\n    onMounted,\n    onWillUpdateProps,\n    useEffect,\n    onWillUnmount,\n    status,\n} from \"@odoo/owl\";\nimport { uniqueId } from '@web/core/utils/functions';\nimport { rpc } from \"@web/core/network/rpc\";\n// Ensure `@web/views/fields/html/html_field` is loaded first as this module\n// must override the html field in the registry.\nimport '@web/views/fields/html/html_field';\nimport { Deferred } from \"@web/core/utils/concurrency\";\n\nlet stripHistoryIds;\n\nexport class HtmlField extends Component {\n    static template = \"web_editor.HtmlField\";\n    static components = {\n        TranslationButton,\n    };\n    static defaultProps = { dynamicPlaceholder: false };\n    static props = {\n        ...standardFieldProps,\n        placeholder: { type: String, optional: true },\n        codeview: { type: Boolean, optional: true },\n        isCollaborative: { type: Boolean, optional: true },\n        dynamicPlaceholder: { type: Boolean, optional: true, default: false },\n        dynamicPlaceholderModelReferenceField: { type: String, optional: true },\n        cssReadonlyAssetId: { type: String, optional: true },\n        isInlineStyle: { type: Boolean, optional: true },\n        sandboxedPreview: {type: Boolean, optional: true},\n        wrapper: { type: String, optional: true },\n        wysiwygOptions: { type: Object },\n        hasReadonlyModifiers: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        this.containsComplexHTML = this.computeContainsComplexHTML();\n        this.sandboxedPreview = this.props.sandboxedPreview || this.containsComplexHTML;\n\n        this.readonlyElementRef = useRef(\"readonlyElement\");\n        this.codeViewRef = useRef(\"codeView\");\n        this.iframeRef = useRef(\"iframe\");\n        this.codeViewButtonRef = useRef(\"codeViewButton\");\n\n        if (this.props.dynamicPlaceholder) {\n            this.dynamicPlaceholder = useDynamicPlaceholder();\n        }\n\n        this.onIframeUpdated = this.env.onIframeUpdated || (() => {});\n\n        this.state = useState({\n            showCodeView: false,\n            iframeVisible: false,\n        });\n\n        const { model } = this.props.record;\n        useBus(model.bus, \"WILL_SAVE_URGENTLY\", () =>\n            this.commitChanges({ urgent: true })\n        );\n        useBus(model.bus, \"NEED_LOCAL_CHANGES\", ({ detail }) =>\n            detail.proms.push(this.commitChanges({ shouldInline: true }))\n        );\n\n        useSpellCheck();\n\n        this._onUpdateIframeId = \"onLoad_\" + uniqueId(\"FieldHtml\");\n\n        onWillStart(async () => {\n            if (this.props.cssReadonlyAssetId) {\n                this.cssReadonlyAsset = await getBundle(this.props.cssReadonlyAssetId);\n            }\n            await this._lazyloadWysiwyg();\n        });\n        this._lastRecordInfo = {\n            res_model: this.props.record.resModel,\n            res_id: this.props.record.resId,\n        };\n        onWillUpdateProps((newProps) => {\n            if (!newProps.readonly && !this.sandboxedPreview && this.state.iframeVisible) {\n                this.state.iframeVisible = false;\n            }\n\n            const newRecordInfo = {\n                res_model: newProps.record.resModel,\n                res_id: newProps.record.resId,\n            };\n            if (JSON.stringify(this._lastRecordInfo) !== JSON.stringify(newRecordInfo)) {\n                this.currentEditingValue = undefined;\n            }\n            this._lastRecordInfo = newRecordInfo;\n        });\n        useEffect(() => {\n            (async () => {\n                if (this._qwebPlugin) {\n                    this._qwebPlugin.destroy();\n                }\n                if (this.props.readonly || (!this.state.showCodeView && this.sandboxedPreview)) {\n                    if (this.showIframe) {\n                        await this._setupReadonlyIframe();\n                    } else if (this.readonlyElementRef.el) {\n                        this._qwebPlugin = new QWebPlugin();\n                        this._qwebPlugin.sanitizeElement(this.readonlyElementRef.el);\n                        // Ensure all external links are opened in a new tab.\n                        retargetLinks(this.readonlyElementRef.el);\n\n                        const hasReadonlyModifiers = this.props.hasReadonlyModifiers;\n                        if (!hasReadonlyModifiers) {\n                            const $el = $(this.readonlyElementRef.el);\n                            $el.off('.checklistBinding');\n                            $el.on('click.checklistBinding', 'ul.o_checklist > li', this._onReadonlyClickChecklist.bind(this));\n                            $el.on('click.checklistBinding', '.o_stars .fa-star, .o_stars .fa-star-o', this._onReadonlyClickStar.bind(this));\n                        }\n                    }\n                } else {\n                    const codeViewEl = this._getCodeViewEl();\n                    if (codeViewEl) {\n                        codeViewEl.value = this.props.record.data[this.props.name];\n                    }\n                }\n            })();\n        });\n        onMounted(() => {\n            this.dynamicPlaceholder?.setElementRef(this.wysiwyg);\n        });\n        onWillUnmount(async () => {\n            if (!this.props.readonly && this._isDirty()) {\n                // If we still have uncommited changes, commit them to avoid losing them.\n                await this.commitChanges();\n            }\n            if (this._qwebPlugin) {\n                this._qwebPlugin.destroy();\n            }\n            if (this.resizerHandleObserver) {\n                this.resizerHandleObserver.disconnect();\n            }\n        });\n    }\n\n    /**\n     * Check whether the current value contains nodes that would break\n     * on insertion inside an existing body.\n     *\n     * @returns {boolean} true if 'this.props.value' contains a node\n     * that can only exist once per document.\n     */\n    computeContainsComplexHTML() {\n        const domParser = new DOMParser();\n        const parsedOriginal = domParser.parseFromString(this.props.record.data[this.props.name] || '', 'text/html');\n        return !!parsedOriginal.head.innerHTML.trim();\n    }\n\n    get isTranslatable() {\n        return this.props.record.fields[this.props.name].translate;\n    }\n    get markupValue () {\n        return this.props.record.data[this.props.name];\n    }\n    get showIframe () {\n        return (this.sandboxedPreview && !this.state.showCodeView) || (this.props.readonly && this.props.cssReadonlyAssetId);\n    }\n    get wysiwygOptions() {\n        let dynamicPlaceholderOptions = {};\n        if (this.props.dynamicPlaceholder) {\n            dynamicPlaceholderOptions = {\n                // Add the powerbox option to open the Dynamic Placeholder\n                // generator.\n                powerboxItems: [\n                    {\n                        category: _t('Marketing Tools'),\n                        name: _t('Dynamic Placeholder'),\n                        priority: 10,\n                        description: _t('Insert a field'),\n                        fontawesome: 'fa-magic',\n                        callback: () => {\n                            this.wysiwygRangePosition = getRangePosition(document.createElement('x'), this.wysiwyg.options.document || document);\n                            this.dynamicPlaceholder.updateModel(this.props.dynamicPlaceholderModelReferenceField);\n                            // The method openDynamicPlaceholder need to be triggered\n                            // after the focus from powerBox prevalidate.\n                            setTimeout(async () => {\n                                await this.dynamicPlaceholder.open(\n                                    {\n                                        validateCallback: this.onDynamicPlaceholderValidate.bind(this),\n                                        closeCallback: this.onDynamicPlaceholderClose.bind(this),\n                                        positionCallback: this.positionDynamicPlaceholder.bind(this),\n                                    }\n                                );\n                            });\n                        },\n                    }\n                ],\n                powerboxFilters: [this._filterPowerBoxCommands.bind(this)],\n            }\n        }\n\n        const wysiwygOptions = {...this.props.wysiwygOptions};\n        const { sanitize_tags, sanitize } = this.props.record.fields[this.props.name];\n        if (sanitize_tags || (sanitize_tags === undefined && sanitize)) {\n            wysiwygOptions.allowCommandVideo = false; // Tag-sanitized fields remove videos.\n        }\n\n        return {\n            value: this.props.record.data[this.props.name],\n            autostart: false,\n            onAttachmentChange: this._onAttachmentChange.bind(this),\n            onDblClickEditableMedia: this._onDblClickEditableMedia.bind(this),\n            onWysiwygBlur: this._onWysiwygBlur.bind(this),\n            ...wysiwygOptions,\n            ...dynamicPlaceholderOptions,\n            recordInfo: {\n                res_model: this.props.record.resModel,\n                res_id: this.props.record.resId,\n            },\n            collaborationChannel: this.props.isCollaborative && {\n                collaborationModelName: this.props.record.resModel,\n                collaborationFieldName: this.props.name,\n                collaborationResId: parseInt(this.props.record.resId),\n            },\n            fieldId: this.props.id,\n            editorPlugins: [...(wysiwygOptions.editorPlugins || []), QWebPlugin, this.MoveNodePlugin],\n            record: this.props.record,\n        };\n    }\n    /**\n     * Prevent usage of the dynamic placeholder command inside widgets\n     * containing background images ( cover & masonry ).\n     *\n     * We cannot use dynamic placeholder in block containing background images\n     * because the email processing will flatten the text into the background\n     * image and this case the dynamic placeholder cannot be dynamic anymore.\n     *\n     * @param {Array} commands commands available in this wysiwyg\n     * @returns {Array} commands which can be used after the filter was applied\n     */\n    _filterPowerBoxCommands(commands) {\n        let selectionIsInForbidenSnippet = false;\n        if (this.wysiwyg && this.wysiwyg.odooEditor) {\n            const selection = this.wysiwyg.odooEditor.document.getSelection();\n            selectionIsInForbidenSnippet = this.wysiwyg.closestElement(\n                selection.anchorNode,\n                'div[data-snippet=\"s_cover\"], div[data-snippet=\"s_masonry_block\"]'\n            );\n        }\n        return selectionIsInForbidenSnippet ? commands.filter((o) => o.title !== \"Dynamic Placeholder\") : commands;\n    }\n\n    getEditingValue () {\n        const codeViewEl = this._getCodeViewEl();\n        if (codeViewEl) {\n            return codeViewEl.value;\n        } else {\n            if (this.wysiwyg) {\n                return this.wysiwyg.getValue();\n            } else {\n                return null;\n            }\n        }\n    }\n    async updateValue() {\n        const value = this.getEditingValue();\n        const lastValue = (this.props.record.data[this.props.name] || \"\").toString();\n        if (\n            value !== null &&\n            !(!lastValue && stripHistoryIds(value) === \"<p><br></p>\") &&\n            stripHistoryIds(value) !== stripHistoryIds(lastValue)\n        ) {\n            this.props.record.model.bus.trigger(\"FIELD_IS_DIRTY\", false);\n            this.currentEditingValue = value;\n            await this.props.record.update({ [this.props.name]: value });\n        }\n    }\n    async startWysiwyg(wysiwyg) {\n        this.wysiwyg = wysiwyg;\n        await this.wysiwyg.startEdition();\n        wysiwyg.$editable[0].classList.add(\"odoo-editor-qweb\");\n\n        if (this.props.codeview) {\n            const $codeviewButtonToolbar = $(`\n                <div id=\"codeview-btn-group\" class=\"btn-group\">\n                    <button class=\"o_codeview_btn btn btn-primary\">\n                        <i class=\"fa fa-code\"></i>\n                    </button>\n                </div>\n            `);\n            this.wysiwyg.toolbarEl.append($codeviewButtonToolbar[0]);\n            $codeviewButtonToolbar.click(this.toggleCodeView.bind(this));\n        }\n        this.wysiwyg.odooEditor.addEventListener(\"historyStep\", () =>\n            this.props.record.model.bus.trigger(\"FIELD_IS_DIRTY\", this._isDirty())\n        );\n\n        if (this.props.isCollaborative) {\n            this.wysiwyg.odooEditor.addEventListener(\"onExternalHistorySteps\", () =>\n                this.props.record.model.bus.trigger(\"FIELD_IS_DIRTY\", this._isDirty())\n            );\n        }\n\n        this.isRendered = true;\n    }\n    /**\n     * Toggle the code view and update the UI.\n     */\n    toggleCodeView() {\n        this.state.showCodeView = !this.state.showCodeView;\n\n        if (this.wysiwyg) {\n            this.wysiwyg.odooEditor.observerUnactive('toggleCodeView');\n            if (this.state.showCodeView) {\n                this.wysiwyg.$editable.remove();\n                this.wysiwyg.odooEditor.toolbarHide();\n                const value = this.wysiwyg.getValue();\n                this.props.record.update({ [this.props.name]: value });\n            } else {\n                this.wysiwyg.odooEditor.observerActive('toggleCodeView');\n            }\n        }\n        if (!this.state.showCodeView) {\n            const $codeview = $(this.codeViewRef.el);\n            const value = $codeview.val();\n            this.props.record.update({ [this.props.name]: value });\n\n        }\n    }\n    onDynamicPlaceholderValidate(chain, defaultValue) {\n        if (chain) {\n            // Ensure the focus is in the editable document\n            // before inserting the <t> element.\n            this.wysiwyg.focus();\n            let dynamicPlaceholder = \"object.\" + chain;\n            const t = document.createElement('T');\n            t.setAttribute('t-out', dynamicPlaceholder);\n            t.innerText = defaultValue;\n            this.wysiwyg.odooEditor.execCommand('insert', t);\n            // Ensure the dynamic placeholder <t> element is sanitized.\n            this.wysiwyg.odooEditor.sanitize(t);\n        }\n    }\n    onDynamicPlaceholderClose() {\n        this.wysiwyg.focus();\n    }\n\n    /**\n     * @param {HTMLElement} popover\n     * @param {Object} position\n     */\n    positionDynamicPlaceholder(popover, position) {\n        // make sure the popover won't be out(below) of the page\n        const enoughSpaceBelow = window.innerHeight - popover.clientHeight - this.wysiwygRangePosition.top;\n        let topPosition = (enoughSpaceBelow > 0) ? this.wysiwygRangePosition.top : this.wysiwygRangePosition.top + enoughSpaceBelow;\n\n        // Offset the popover to ensure the arrow is pointing at\n        // the precise range location.\n        let leftPosition = this.wysiwygRangePosition.left - 14;\n        // make sure the popover won't be out(right) of the page\n        const enoughSpaceRight = window.innerWidth - popover.clientWidth - leftPosition;\n        leftPosition = (enoughSpaceRight > 0) ? leftPosition : leftPosition + enoughSpaceRight;\n\n        // Apply the position back to the element.\n        popover.style.top = topPosition + 'px';\n        popover.style.left = leftPosition + 'px';\n    }\n    async commitChanges({ urgent, shouldInline } = {}) {\n        if (this.isCurrentlySaving && !urgent) {\n            await this.isCurrentlySaving;\n        }\n        if (this._isDirty() || urgent || (shouldInline && this.props.isInlineStyle)) {\n            let savePendingImagesPromise, toInlinePromise;\n            if (this.wysiwyg && this.wysiwyg.odooEditor) {\n                if (!urgent) {\n                    this.isCurrentlySaving = new Deferred();\n                }\n                this.wysiwyg.odooEditor.observerUnactive('commitChanges');\n                savePendingImagesPromise = this.wysiwyg.savePendingImages();\n                if (this.props.isInlineStyle) {\n                    // Avoid listening to changes made during the _toInline process.\n                    toInlinePromise = this._toInline();\n                }\n                if (urgent && status(this) !== 'destroyed') {\n                    await this.updateValue();\n                }\n                await savePendingImagesPromise;\n                const codeViewEl = this._getCodeViewEl();\n                if (codeViewEl) {\n                    codeViewEl.value = this.wysiwyg.getValue();\n                }\n                if (this.props.isInlineStyle) {\n                    await toInlinePromise;\n                }\n                this.wysiwyg.odooEditor.observerActive('commitChanges');\n            }\n            if (status(this) !== 'destroyed') {\n                await this.updateValue();\n            }\n            if (this.isCurrentlySaving) {\n                this.isCurrentlySaving.resolve();\n            }\n        }\n    }\n    async _lazyloadWysiwyg() {\n        // In some bundle (eg. `web.qunit_suite_tests`), the following module is already included.\n        let wysiwygModule = await odoo.loader.modules.get('@web_editor/js/wysiwyg/wysiwyg');\n        this.MoveNodePlugin = (await odoo.loader.modules.get('@web_editor/js/wysiwyg/MoveNodePlugin'))?.MoveNodePlugin;\n        // Otherwise, load the module.\n        if (!wysiwygModule) {\n            await ensureJQuery();\n            await loadBundle('web_editor.backend_assets_wysiwyg');\n            wysiwygModule = await odoo.loader.modules.get('@web_editor/js/wysiwyg/wysiwyg');\n            this.MoveNodePlugin = (await odoo.loader.modules.get('@web_editor/js/wysiwyg/MoveNodePlugin')).MoveNodePlugin;\n        }\n        stripHistoryIds = wysiwygModule.stripHistoryIds;\n        this.Wysiwyg = wysiwygModule.Wysiwyg;\n    }\n    _isDirty() {\n        const strippedPropValue = stripHistoryIds(String(this.props.record.data[this.props.name]));\n        const strippedEditingValue = stripHistoryIds(this.getEditingValue());\n        const domParser = new DOMParser();\n        const codeViewEl = this._getCodeViewEl();\n        let parsedPreviousValue;\n        // If the wysiwyg is active, we need to clean the content of the\n        // initialValue as the editingValue will be cleaned.\n        if (!codeViewEl && this.wysiwyg) {\n            const editable = domParser.parseFromString(strippedPropValue || '<p><br></p>', 'text/html').body;\n            // Temporarily append the editable to the DOM because the\n            // wysiwyg.getValue can indirectly call methods that needs to have\n            // access the node.ownerDocument.defaultView.getComputedStyle.\n            // By appending the editable to the dom, the node.ownerDocument will\n            // have a `defaultView`.\n            const div = document.createElement('div');\n            div.style.display = 'none';\n            div.append(editable);\n            document.body.append(div);\n            const editableValue = stripHistoryIds(this.wysiwyg.getValue({ $layout: $(editable) }));\n            div.remove();\n            parsedPreviousValue = domParser.parseFromString(editableValue, 'text/html').body;\n        } else {\n            parsedPreviousValue = domParser.parseFromString(strippedPropValue || '<p><br></p>', 'text/html').body;\n        }\n        const parsedNewValue = domParser.parseFromString(strippedEditingValue, 'text/html').body;\n        return !this.props.readonly && parsedPreviousValue.innerHTML !== parsedNewValue.innerHTML;\n    }\n    _getCodeViewEl() {\n        return this.state.showCodeView && this.codeViewRef.el;\n    }\n    async _setupReadonlyIframe() {\n        const iframeTarget = this.sandboxedPreview\n            ? this.iframeRef.el.contentDocument.documentElement\n            : this.iframeRef.el.contentDocument.querySelector('#iframe_target');\n\n        if (this.iframePromise && iframeTarget) {\n            if (iframeTarget.innerHTML !== this.props.record.data[this.props.name]) {\n                iframeTarget.innerHTML = this.props.record.data[this.props.name];\n                retargetLinks(iframeTarget);\n            }\n            return this.iframePromise;\n        }\n        this.iframePromise = new Promise((resolve) => {\n            let value = this.props.record.data[this.props.name];\n\n            // this bug only appears on some computers with some chrome version.\n            let avoidDoubleLoad = 0;\n\n            // inject content in iframe\n            window.top[this._onUpdateIframeId] = (_avoidDoubleLoad) => {\n                if (_avoidDoubleLoad !== avoidDoubleLoad) {\n                    console.warn('Wysiwyg iframe double load detected');\n                    return;\n                }\n                resolve();\n                this.state.iframeVisible = true;\n                this.onIframeUpdated();\n            };\n\n            this.iframeRef.el.addEventListener('load', async () => {\n                const _avoidDoubleLoad = ++avoidDoubleLoad;\n\n                if (_avoidDoubleLoad !== avoidDoubleLoad) {\n                    console.warn('Wysiwyg immediate iframe double load detected');\n                    return;\n                }\n                const cwindow = this.iframeRef.el.contentWindow;\n                try {\n                    cwindow.document;\n                } catch {\n                    return;\n                }\n                if (!this.sandboxedPreview) {\n                    cwindow.document\n                        .open(\"text/html\", \"replace\")\n                        .write(\n                            '<!DOCTYPE html><html>' +\n                            '<head>' +\n                                '<meta charset=\"utf-8\"/>' +\n                                '<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"/>\\n' +\n                                '<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\"/>\\n' +\n                            '</head>\\n' +\n                            '<body class=\"o_in_iframe o_readonly\" style=\"overflow: hidden;\">\\n' +\n                                '<div id=\"iframe_target\"></div>\\n' +\n                            '</body>' +\n                            '</html>');\n                }\n                if (this.props.cssReadonlyAssetId) {\n                    for (const cssLib of this.cssReadonlyAsset.cssLibs) {\n                        const link = cwindow.document.createElement('link');\n                        link.setAttribute('type', 'text/css');\n                        link.setAttribute('rel', 'stylesheet');\n                        link.setAttribute('href', cssLib);\n                        cwindow.document.head.append(link);\n                    }\n                }\n\n                if (!this.sandboxedPreview) {\n                    const iframeTarget = cwindow.document.querySelector('#iframe_target');\n                    iframeTarget.innerHTML = value;\n\n                    const script = cwindow.document.createElement('script');\n                    script.setAttribute('type', 'text/javascript');\n                    const scriptTextNode = document.createTextNode(\n                        `if (window.top.${this._onUpdateIframeId}) {` +\n                            `window.top.${this._onUpdateIframeId}(${_avoidDoubleLoad})` +\n                        `}`\n                    );\n                    script.append(scriptTextNode);\n                    cwindow.document.body.append(script);\n                } else {\n                    cwindow.document.documentElement.innerHTML = value;\n                }\n\n                const height = cwindow.document.body.scrollHeight;\n                this.iframeRef.el.style.height = Math.max(30, Math.min(height, 500)) + 'px';\n\n                retargetLinks(cwindow.document.body);\n                if (this.sandboxedPreview) {\n                    this.state.iframeVisible = true;\n                    this.onIframeUpdated();\n                    resolve();\n                }\n            });\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        });\n        return this.iframePromise;\n    }\n    /**\n     * Converts CSS dependencies to CSS-independent HTML.\n     * - CSS display for attachment link -> real image\n     * - Font icons -> images\n     * - CSS styles -> inline styles\n     *\n     * @private\n     */\n    async _toInline() {\n        const $editable = this.wysiwyg.getEditable();\n        this.wysiwyg.odooEditor.sanitize(this.wysiwyg.odooEditor.editable);\n        const html = this.wysiwyg.getValue();\n        const $odooEditor = $editable.closest('.odoo-editor-editable');\n        // Save correct nodes references.\n        // Remove temporarily the class so that css editing will not be converted.\n        $odooEditor.removeClass('odoo-editor-editable');\n        $editable.html(html);\n        await toInline($editable, { $iframe: this.wysiwyg.$iframe, wysiwyg:this.wysiwyg });\n        $odooEditor.addClass('odoo-editor-editable');\n\n        this.wysiwyg.setValue($editable.html());\n        this.wysiwyg.odooEditor.sanitize(this.wysiwyg.odooEditor.editable);\n    }\n    _onAttachmentChange(attachment) {\n        // This only needs to happen for the composer for now\n        if (!(this.props.record.fieldNames.includes('attachment_ids') && this.props.record.resModel === 'mail.compose.message')) {\n            return;\n        }\n        this.props.record.data.attachment_ids.linkTo(attachment.id, attachment);\n    }\n    _onDblClickEditableMedia(ev) {\n        const el = ev.currentTarget;\n        if (el.nodeName === 'IMG' && el.src) {\n            this.wysiwyg.showImageFullscreen(el.src);\n        }\n    }\n    _onWysiwygBlur() {\n        // Avoid save on blur if the html field is in inline mode.\n        if (this.props.isInlineStyle) {\n            this.updateValue();\n        } else {\n            this.commitChanges();\n        }\n    }\n    async _onReadonlyClickChecklist(ev) {\n        if (ev.offsetX > 0) {\n            return;\n        }\n        ev.stopPropagation();\n        ev.preventDefault();\n        const checked = $(ev.target).hasClass('o_checked');\n        let checklistId = $(ev.target).attr('id');\n        checklistId = checklistId && checklistId.replace('checkId-', '');\n        checklistId = parseInt(checklistId || '0');\n\n        const value = await rpc('/web_editor/checklist', {\n            res_model: this.props.record.resModel,\n            res_id: this.props.record.resId,\n            filename: this.props.name,\n            checklistId: checklistId,\n            checked: !checked,\n        });\n        if (value) {\n            this.props.record.update({ [this.props.name]: value });\n        }\n    }\n    async _onReadonlyClickStar(ev) {\n        ev.stopPropagation();\n        ev.preventDefault();\n\n        const node = ev.target;\n        const previousStars = getAdjacentPreviousSiblings(node, sib => (\n            sib.nodeType === Node.ELEMENT_NODE && sib.className.includes('fa-star')\n        ));\n        const nextStars = getAdjacentNextSiblings(node, sib => (\n            sib.nodeType === Node.ELEMENT_NODE && sib.classList.contains('fa-star')\n        ));\n        const shouldToggleOff = node.classList.contains('fa-star') && !nextStars.length;\n        const rating = shouldToggleOff ? 0 : previousStars.length + 1;\n\n        let starsId = $(node).parent().attr('id');\n        starsId = starsId && starsId.replace('checkId-', '');\n        starsId = parseInt(starsId || '0');\n        const value = await rpc('/web_editor/stars', {\n            res_model: this.props.record.resModel,\n            res_id: this.props.record.resId,\n            filename: this.props.name,\n            starsId,\n            rating,\n        });\n        if (value) {\n            this.props.record.update({ [this.props.name]: value });\n        }\n    }\n}\n\nexport const htmlField = {\n    component: HtmlField,\n    displayName: _t(\"Html\"),\n    supportedOptions: [{\n        label: _t(\"CSS Edit\"),\n        name: \"cssEdit\",\n        type: \"string\"\n    }, {\n        label: _t(\"Height\"),\n        name: \"height\",\n        type: \"string\"\n    }, {\n        label: _t(\"Min height\"),\n        name: \"minHeight\",\n        type: \"string\"\n    }, {\n        label: _t(\"Max height\"),\n        name: \"maxHeight\",\n        type: \"string\"\n    }, {\n        label: _t(\"Snippets\"),\n        name: \"snippets\",\n        type: \"string\"\n    }, {\n        label: _t(\"No videos\"),\n        name: \"noVideos\",\n        type: \"boolean\",\n        default: true\n    }, {\n        label: _t(\"Resizable\"),\n        name: \"resizable\",\n        type: \"boolean\",\n    }, {\n        label: _t(\"Sandboxed preview\"),\n        name: \"sandboxedPreview\",\n        type: \"boolean\",\n        help: _t(\"With the option enabled, all content can only be viewed in a sandboxed iframe or in the code editor.\"),\n    }, {\n        label: _t(\"Collaborative edition\"),\n        name: \"collaborative\",\n        type: \"boolean\",\n    },{\n        label: _t(\"Collaborative trigger\"),\n        name: \"collaborative_trigger\",\n        type: \"selection\",\n        choices: [\n            { label: _t(\"Focus\"), value: \"focus\" },\n            { label: _t(\"Start\"), value: \"start\" },\n        ],\n        default: \"focus\",\n        help: _t(\"Specify when the collaboration starts. 'Focus' will start the collaboration session when the user clicks inside the text field (default), 'Start' when the record is loaded (could impact performance if set).\"),\n    }, {\n        label: _t(\"Codeview\"),\n        name: \"codeview\",\n        type: \"boolean\",\n        help: _t(\"Allow users to view and edit the field in HTML.\")\n    }],\n    supportedTypes: [\"html\"],\n    extractProps({ attrs, options }, dynamicInfo) {\n        const wysiwygOptions = {\n            placeholder: attrs.placeholder,\n            noAttachment: options['no-attachment'],\n            inIframe: Boolean(options.cssEdit),\n            iframeCssAssets: options.cssEdit,\n            iframeHtmlClass: attrs.iframeHtmlClass,\n            snippets: options.snippets,\n            mediaModalParams: {\n                noVideos: 'noVideos' in options ? options.noVideos : true,\n                useMediaLibrary: true,\n            },\n            linkOptions: {\n                forceNewWindow: true,\n            },\n            tabsize: 0,\n            height: options.height,\n            minHeight: options.minHeight,\n            maxHeight: options.maxHeight,\n            resizable: 'resizable' in options ? options.resizable : false,\n        };\n        if ('collaborative' in options) {\n            wysiwygOptions.collaborative = options.collaborative;\n            // Two supported triggers:\n            // 'start': Join the peerToPeer connection immediately\n            // 'focus': Join when the editable has focus\n            wysiwygOptions.collaborativeTrigger = options.collaborative_trigger || 'focus';\n        }\n\t    if ('style-inline' in options) {\n\t        wysiwygOptions.inlineStyle = Boolean(options['style-inline']);\n\t    }\n        if ('allowCommandImage' in options) {\n            // Set the option only if it is explicitly set in the view so a default\n            // can be set elsewhere otherwise.\n            wysiwygOptions.allowCommandImage = Boolean(options.allowCommandImage);\n        }\n        if ('allowCommandVideo' in options) {\n            // Set the option only if it is explicitly set in the view so a default\n            // can be set elsewhere otherwise.\n            wysiwygOptions.allowCommandVideo = Boolean(options.allowCommandVideo);\n        }\n        return {\n            codeview: Boolean(odoo.debug && options.codeview),\n            placeholder: attrs.placeholder,\n            sandboxedPreview: Boolean(options.sandboxedPreview),\n\n            isCollaborative: options.collaborative,\n            cssReadonlyAssetId: options.cssReadonly,\n            dynamicPlaceholder: options?.dynamic_placeholder || false,\n            dynamicPlaceholderModelReferenceField: options?.dynamic_placeholder_model_reference_field || \"\",\n            isInlineStyle: options['style-inline'],\n\n            wysiwygOptions,\n            hasReadonlyModifiers: dynamicInfo.readonly,\n        };\n    },\n    additionalClasses: [\"o_field_html\"],\n};\n\nregistry.category(\"fields\").add(\"html_legacy\", htmlField, { force: true });\n\n// Ensure all links are opened in a new tab.\nconst retargetLinks = (container) => {\n    for (const link of container.querySelectorAll('a')) {\n        link.setAttribute('target', '_blank');\n        link.setAttribute('rel', 'noreferrer');\n    }\n}\n", "/** @odoo-module **/\n\nimport { PortalWizardUserListController } from \"../list/portal_wizard_user_list_controller\";\nimport { X2ManyField, x2ManyField } from \"@web/views/fields/x2many/x2many_field\";\nimport { registry } from \"@web/core/registry\";\n\nexport class PortalUserX2ManyField extends X2ManyField {\n    static components = {\n        ...X2ManyField.components,\n        Controller: PortalWizardUserListController,\n    };\n}\n\nexport const portalUserX2ManyField = {\n    ...x2ManyField,\n    component: PortalUserX2ManyField,\n};\n\nregistry.category(\"fields\").add(\"portal_wizard_user_one2many\", portalUserX2ManyField);\n", "/** @odoo-module **/\n\nimport { ListController } from \"@web/views/list/list_controller\";\n\nexport class PortalWizardUserListController extends ListController {\n    setup() {\n        super.setup();\n        this.isPortalActionOngoing = false;\n    }\n\n    /**\n     * @override\n     */\n     async beforeExecuteActionButton(clickParams) {\n        if (clickParams.name === 'action_refresh_modal' || this.isPortalActionOngoing) {\n            return false;\n        }\n        this.isPortalActionOngoing = true;\n        return super.beforeExecuteActionButton(clickParams);\n    }\n    \n    /**\n     * @override\n     */\n    async afterExecuteActionButton(clickParams) {\n        this.isPortalActionOngoing = false;\n    }\n}\n", "/** @odoo-module */\n\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { useEffect } from \"@odoo/owl\";\n\nexport class SectionListRenderer extends ListRenderer {\n    setup() {\n        super.setup();\n\n        this.displayType = \"line_section\";\n        this.titleField = \"title\";\n\n        useEffect(\n            (table) => {\n                if (table) {\n                    table.classList.add(\"o_section_list_view\");\n                }\n            },\n            () => [this.tableRef.el]\n        );\n    }\n\n    getColumns(record) {\n        const columns = super.getColumns(record);\n        if (this.isSection(record)) {\n            return this.getSectionColumns(columns);\n        }\n        return columns;\n    }\n\n    getRowClass(record) {\n        const classNames = super.getRowClass(record).split(\" \");\n        if (this.isSection(record)) {\n            classNames.push(`o_is_${this.displayType}`, `fw-bold`);\n        }\n        return classNames.join(\" \");\n    }\n\n    getSectionColumns(columns) {\n        const sectionColumns = columns.filter((col) => col.widget === \"handle\");\n        let colspan = columns.length - sectionColumns.length;\n        if (this.activeActions.onDelete) {\n            colspan++;\n        }\n        const titleCol = columns.find(\n            (col) => col.type === \"field\" && col.name === this.titleField\n        );\n        sectionColumns.push({ ...titleCol, colspan });\n        return sectionColumns;\n    }\n\n    isSection(record) {\n        return record.data.display_type === this.displayType;\n    }\n}\nSectionListRenderer.recordRowTemplate = \"resource.SectionListRenderer.RecordRow\";\n", "/** @odoo-module */\n\nimport { SectionListRenderer } from \"./section_list_renderer\";\nimport { registry } from \"@web/core/registry\";\nimport { X2ManyField, x2ManyField } from \"@web/views/fields/x2many/x2many_field\";\n\nclass SectionOneToManyField extends X2ManyField {\n    static components = {\n        ...X2ManyField.components,\n        ListRenderer: SectionListRenderer,\n    };\n    static defaultProps = {\n        ...X2ManyField.defaultProps,\n        editable: \"bottom\",\n    };\n}\n\nregistry.category(\"fields\").add(\"section_one2many\", {\n    ...x2ManyField,\n    component: SectionOneToManyField,\n    additionalClasses: [...x2ManyField.additionalClasses || [], \"o_field_one2many\"],\n});\n", "import { useState } from \"@odoo/owl\";\nimport { FormController } from \"@web/views/form/form_controller\";\n\nexport class FormControllerWithHTMLExpander extends FormController {\n    static template = \"resource.FormViewWithHtmlExpander\";\n\n    setup() {\n        super.setup();\n        this.htmlExpanderState = useState({ reload: true });\n        const oldOnNotebookPageChange = this.onNotebookPageChange;\n        this.onNotebookPageChange = (notebookId, page) => {\n            oldOnNotebookPageChange(notebookId, page);\n            if (page && !this.htmlExpanderState.reload) {\n                this.htmlExpanderState.reload = true;\n            }\n        };\n    }\n\n    get modelParams() {\n        const modelParams = super.modelParams;\n        const onRootLoaded = modelParams.hooks.onRootLoaded;\n        modelParams.hooks.onRootLoaded = async () => {\n            if (onRootLoaded) {\n                onRootLoaded();\n            }\n            this.htmlExpanderState.reload = true;\n        };\n        return modelParams;\n    }\n\n    notifyHTMLFieldExpanded() {\n        this.htmlExpanderState.reload = false;\n    }\n\n    async onRecordSaved(record, changes) {\n        super.onRecordSaved(record, changes);\n        this.htmlExpanderState.reload = true;\n    }\n}\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { FormRenderer } from \"@web/views/form/form_renderer\";\nimport { useRef, useEffect } from \"@odoo/owl\";\n\nexport class FormRendererWithHtmlExpander extends FormRenderer {\n    static props = {\n        ...FormRenderer.props,\n        reloadHtmlFieldHeight: { type: Boolean, optional: true },\n        notifyHtmlExpander: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        ...FormRenderer.defaultProps,\n        reloadHtmlFieldHeight: true,\n        notifyHtmlExpander: () => {},\n    };\n\n    setup() {\n        super.setup();\n        if (!this.uiService) {\n            // Should be defined in FormRenderer\n            this.uiService = useService(\"ui\");\n        }\n        const ref = useRef(\"compiled_view_root\");\n        useEffect(\n            (el, size) => {\n                if (el && this._canExpandHTMLField(size)) {\n                    const descriptionField = el.querySelector(this.htmlFieldQuerySelector);\n                    if (descriptionField) {\n                        const containerEL = descriptionField.closest(\n                            this.getHTMLFieldContainerQuerySelector\n                        );\n                        const editor = descriptionField.querySelector(\".note-editable\");\n                        const elementToResize = editor || descriptionField;\n                        const { top, bottom } = elementToResize.getBoundingClientRect();\n                        const { bottom: containerBottom } = containerEL.getBoundingClientRect();\n                        const { paddingTop, paddingBottom } = window.getComputedStyle(containerEL);\n                        const nonEditableHeight =\n                            containerBottom -\n                            bottom +\n                            parseInt(paddingTop) +\n                            parseInt(paddingBottom);\n                        const minHeight =\n                            document.documentElement.clientHeight - top - nonEditableHeight;\n                        elementToResize.style.minHeight = `${minHeight}px`;\n                    }\n                }\n                this.props.notifyHtmlExpander();\n            },\n            () => [ref.el, this.uiService.size, this.props.reloadHtmlFieldHeight]\n        );\n    }\n\n    get htmlFieldQuerySelector() {\n        return \".o_field_html[name=description]\";\n    }\n\n    get getHTMLFieldContainerQuerySelector() {\n        return \".o_form_sheet\";\n    }\n\n    _canExpandHTMLField(size) {\n        return size === 6;\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { formView } from \"@web/views/form/form_view\";\nimport { FormRendererWithHtmlExpander } from \"./form_renderer_with_html_expander\";\nimport { FormControllerWithHTMLExpander } from \"./form_controller_with_html_expander\";\n\nexport const formViewWithHtmlExpander = {\n    ...formView,\n    Controller: FormControllerWithHTMLExpander,\n    Renderer: FormRendererWithHtmlExpander,\n};\n\nregistry.category(\"views\").add(\"form_description_expander\", formViewWithHtmlExpander);\n", "import {Component} from \"@odoo/owl\";\nimport {registry} from \"@web/core/registry\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nexport class AccountBatchSendingSummary extends Component {\n    static template = \"account.BatchSendingSummary\";\n    static props = {\n        ...standardFieldProps,\n    };\n\n    setup() {\n        super.setup();\n        this.data = this.props.record.data[this.props.name];\n    }\n}\n\nexport const accountBatchSendingSummary = {\n    component: AccountBatchSendingSummary,\n}\n\nregistry.category(\"fields\").add(\"account_batch_sending_summary\", accountBatchSendingSummary);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { DocumentFileUploader } from \"../document_file_uploader/document_file_uploader\";\n\nexport class AccountFileUploader extends DocumentFileUploader {\n    static template = \"account.AccountFileUploader\";\n    static props = {\n        ...DocumentFileUploader.props,\n        btnClass: { type: String, optional: true },\n        linkText: { type: String, optional: true },\n        togglerTemplate: { type: String, optional: true },\n    };\n\n    getExtraContext() {\n        const extraContext = super.getExtraContext();\n        const record_data = this.props.record ? this.props.record.data : false;\n        return record_data ? {\n            ...extraContext,\n            default_journal_id: record_data.id,\n            default_move_type: (\n                (record_data.type === 'sale' && 'out_invoice')\n                || (record_data.type === 'purchase' && 'in_invoice')\n                || 'entry'\n            ),\n        } : extraContext;\n\n    }\n\n    getResModel() {\n        return \"account.journal\";\n    }\n}\n\n//when file uploader is used on account.journal (with a record)\nexport const accountFileUploader = {\n    component: AccountFileUploader,\n    extractProps: ({ attrs }) => ({\n        togglerTemplate: attrs.template || \"account.JournalUploadLink\",\n        btnClass: attrs.btnClass || \"\",\n        linkText: attrs.title || _t(\"Upload\"),\n    }),\n    fieldDependencies: [\n        { name: \"id\", type: \"integer\" },\n        { name: \"type\", type: \"selection\" },\n    ],\n};\n\nregistry.category(\"view_widgets\").add(\"account_file_uploader\", accountFileUploader);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { Many2OneBarcodeField, many2OneBarcodeField } from \"@web/views/fields/many2one_barcode/many2one_barcode_field\";\n\nexport class AccountMany2oneBarcode extends Many2OneBarcodeField {\n    get hasExternalButton() {\n        // Inspired by sol_product_many2one to display external button despite no_open\n        const res = super.hasExternalButton;\n        return res || (!!this.props.record.data[this.props.name] && !this.state.isFloating);\n    }\n}\n\nregistry.category(\"fields\").add(\"account_many2one_barcode\", {\n    ...many2OneBarcodeField,\n    component: AccountMany2oneBarcode,\n});\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport {\n    SectionAndNoteListRenderer,\n    SectionAndNoteFieldOne2Many,\n    sectionAndNoteFieldOne2Many,\n} from \"../section_and_note_fields_backend/section_and_note_fields_backend\";\n\nexport class AccountMergeWizardLinesRenderer extends SectionAndNoteListRenderer {\n    setup() {\n        super.setup();\n        this.titleField = \"info\";\n    }\n\n    getCellClass(column, record) {\n        const classNames = super.getCellClass(column, record);\n        // Even though the `is_selected` field is invisible for section lines, we should\n        // keep its column (which would be hidden by the call to super.getCellClass)\n        // in order to align the section header name with the account names.\n        if (this.isSectionOrNote(record) && column.name === \"is_selected\") {\n            return classNames.replace(\" o_hidden\", \"\");\n        }\n        return classNames;\n    }\n\n    /** @override **/\n    getSectionColumns(columns) {\n        const sectionCols = columns.filter(\n            (col) =>\n                col.type === \"field\" && (col.name === this.titleField || col.name === \"is_selected\")\n        );\n        return sectionCols.map((col) => {\n            if (col.name === this.titleField) {\n                return { ...col, colspan: columns.length - sectionCols.length + 1 };\n            } else {\n                return { ...col };\n            }\n        });\n    }\n\n    /** @override */\n    isSortable(column) {\n        // Don't allow sorting columns, as that doesn't make sense in the wizard view.\n        return false;\n    }\n}\n\nexport class AccountMergeWizardLinesOne2Many extends SectionAndNoteFieldOne2Many {\n    static components = {\n        ...SectionAndNoteFieldOne2Many.components,\n        ListRenderer: AccountMergeWizardLinesRenderer,\n    };\n}\n\nexport const accountMergeWizardLinesOne2Many = {\n    ...sectionAndNoteFieldOne2Many,\n    component: AccountMergeWizardLinesOne2Many,\n};\n\nregistry\n    .category(\"fields\")\n    .add(\"account_merge_wizard_lines_one2many\", accountMergeWizardLinesOne2Many);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { createElement, append } from \"@web/core/utils/xml\";\nimport { Notebook } from \"@web/core/notebook/notebook\";\nimport { formView } from \"@web/views/form/form_view\";\nimport { FormCompiler } from \"@web/views/form/form_compiler\";\nimport { FormRenderer } from \"@web/views/form/form_renderer\";\nimport { FormController } from '@web/views/form/form_controller';\nimport { useService } from \"@web/core/utils/hooks\";\nimport {_t} from \"@web/core/l10n/translation\";\n\n\nexport class AccountMoveFormController extends FormController {\n    setup() {\n        super.setup();\n        this.account_move_service = useService(\"account_move\");\n    }\n\n    get cogMenuProps() {\n        return {\n            ...super.cogMenuProps,\n            printDropdownTitle: _t(\"Download\"),\n            loadExtraPrintItems: this.loadExtraPrintItems.bind(this),\n        };\n    }\n\n    async loadExtraPrintItems() {\n        if (!this.model.root.isNew) {\n            return []\n        }\n        return this.orm.call(\"account.move\", \"get_extra_print_items\", [this.model.root.resId]);\n    }\n\n\n    async deleteRecord() {\n        if ( !await this.account_move_service.addDeletionDialog(this, this.model.root.resId)) {\n            return super.deleteRecord(...arguments);\n        }\n    }\n}\n\nexport class AccountMoveFormNotebook extends Notebook {\n    static template = \"account.AccountMoveFormNotebook\";\n    static props = {\n        ...Notebook.props,\n        onBeforeTabSwitch: { type: Function, optional: true },\n    };\n\n    async changeTabTo(page_id) {\n        if (this.props.onBeforeTabSwitch) {\n            await this.props.onBeforeTabSwitch(page_id);\n        }\n        this.state.currentPage = page_id;\n    }\n}\n\nexport class AccountMoveFormRenderer extends FormRenderer {\n    static components = {\n        ...FormRenderer.components,\n        AccountMoveFormNotebook: AccountMoveFormNotebook,\n    };\n\n    async saveBeforeTabChange() {\n        if (this.props.record.isInEdition && await this.props.record.isDirty()) {\n            const contentEl = document.querySelector('.o_content');\n            const scrollPos = contentEl.scrollTop;\n            await this.props.record.save();\n            if (scrollPos) {\n                contentEl.scrollTop = scrollPos;\n            }\n        }\n    }\n}\n\nexport class AccountMoveFormCompiler extends FormCompiler {\n    compileNotebook(el, params) {\n        const originalNoteBook = super.compileNotebook(...arguments);\n        const noteBook = createElement(\"AccountMoveFormNotebook\");\n        for (const attr of originalNoteBook.attributes) {\n            noteBook.setAttribute(attr.name, attr.value);\n        }\n        noteBook.setAttribute(\"onBeforeTabSwitch\", \"() => __comp__.saveBeforeTabChange()\");\n        const slots = originalNoteBook.childNodes;\n        append(noteBook, [...slots]);\n        return noteBook;\n    }\n}\n\nexport const AccountMoveFormView = {\n    ...formView,\n    Renderer: AccountMoveFormRenderer,\n    Compiler: AccountMoveFormCompiler,\n    Controller: AccountMoveFormController,\n};\n\nregistry.category(\"views\").add(\"account_move_form\", AccountMoveFormView);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { parseDate, formatDate } from \"@web/core/l10n/dates\";\n\nimport { formatMonetary } from \"@web/views/fields/formatters\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { Component } from \"@odoo/owl\";\n\nclass AccountPaymentPopOver extends Component {\n    static props = { \"*\": { optional: true } };\n    static template = \"account.AccountPaymentPopOver\";\n}\n\nexport class AccountPaymentField extends Component {\n    static props = { ...standardFieldProps };\n    static template = \"account.AccountPaymentField\";\n\n    setup() {\n        const position = localization.direction === \"rtl\" ? \"bottom\" : \"left\";\n        this.popover = usePopover(AccountPaymentPopOver, { position });\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n    }\n\n    getInfo() {\n        const info = this.props.record.data[this.props.name] || {\n            content: [],\n            outstanding: false,\n            title: \"\",\n            move_id: this.props.record.resId,\n        };\n        for (const [key, value] of Object.entries(info.content)) {\n            value.index = key;\n            value.amount_formatted = formatMonetary(value.amount, {\n                currencyId: value.currency_id,\n            });\n            if (value.date) {\n                // value.date is a string, parse to date and format to the users date format\n                value.date = formatDate(parseDate(value.date));\n            }\n        }\n        return {\n            lines: info.content,\n            outstanding: info.outstanding,\n            title: info.title,\n            moveId: info.move_id,\n        };\n    }\n\n    onInfoClick(ev, line) {\n        this.popover.open(ev.currentTarget, {\n            title: _t(\"Journal Entry Info\"),\n            ...line,\n            _onRemoveMoveReconcile: this.removeMoveReconcile.bind(this),\n            _onOpenMove: this.openMove.bind(this),\n        });\n    }\n\n    async assignOutstandingCredit(moveId, id) {\n        await this.orm.call(this.props.record.resModel, 'js_assign_outstanding_line', [moveId, id], {});\n        await this.props.record.model.root.load();\n    }\n\n    async removeMoveReconcile(moveId, partialId) {\n        this.popover.close();\n        await this.orm.call(this.props.record.resModel, 'js_remove_outstanding_partial', [moveId, partialId], {});\n        await this.props.record.model.root.load();\n    }\n\n    async openMove(moveId) {\n        const action = await this.orm.call(this.props.record.resModel, 'action_open_business_doc', [moveId], {});\n        this.action.doAction(action);\n    }\n}\n\nexport const accountPaymentField = {\n    component: AccountPaymentField,\n    supportedTypes: [\"char\"],\n};\n\nregistry.category(\"fields\").add(\"payment\", accountPaymentField);\n", "import {HtmlField, htmlField} from \"@web_editor/js/backend/html_field\";\nimport {registry} from \"@web/core/registry\";\n\nexport class AccountPaymentRegisterHtmlField extends HtmlField {\n    static template = \"account.AccountPaymentRegisterHtmlField\";\n\n    async switchInstallmentsAmount(ev) {\n        if (ev.srcElement.classList.contains(\"installments_switch_button\")) {\n            const root = this.env.model.root;\n            await root.update({amount: root.data.installments_switch_amount});\n        }\n    }\n}\n\nexport const accountPaymentRegisterHtmlField = {\n    ...htmlField,\n    component: AccountPaymentRegisterHtmlField,\n};\n\nregistry.category(\"fields\").add(\"account_payment_register_html\", accountPaymentRegisterHtmlField);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\n\nimport { X2ManyField, x2ManyField } from \"@web/views/fields/x2many/x2many_field\";\nimport { useAddInlineRecord } from \"@web/views/fields/relational_utils\";\n\nexport class PaymentTermLineIdsOne2Many extends X2ManyField {\n    setup() {\n        super.setup();\n        // Overloads the addInLine method to mark all new records as 'dirty' by calling update with an empty object.\n        // This prevents the records from being abandoned if the user clicks globally or on an existing record.\n        this.addInLine = useAddInlineRecord({\n            addNew: async (...args) => {\n                const newRecord = await this.list.addNewRecord(...args);\n                newRecord.update({});\n            }\n        });\n    }\n}\n\nexport const PaymentTermLineIds = {\n    ...x2ManyField,\n    component: PaymentTermLineIdsOne2Many,\n}\n\nregistry.category(\"fields\").add(\"payment_term_line_ids\", PaymentTermLineIds);\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { Component } from \"@odoo/owl\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nclass ChangeLine extends Component {\n    static template = \"account.ResequenceChangeLine\";\n    static props = [\"changeLine\", \"ordering\"];\n}\n\nclass ShowResequenceRenderer extends Component {\n    static template = \"account.ResequenceRenderer\";\n    static components = { ChangeLine };\n    static props = { ...standardFieldProps };\n    getValue() {\n        const value = this.props.record.data[this.props.name];\n        return value ? JSON.parse(value) : { changeLines: [], ordering: \"date\" };\n    }\n}\n\nregistry.category(\"fields\").add(\"account_resequence_widget\", {\n    component: ShowResequenceRenderer,\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { statusBarField, StatusBarField } from \"@web/views/fields/statusbar/statusbar_field\";\n\nexport class AccountMoveStatusBarSecuredField extends StatusBarField {\n    static template = \"account.MoveStatusBarSecuredField\";\n\n    get isSecured() {\n        return this.props.record.data['secured'];\n    }\n\n    get currentItem() {\n        return this.getAllItems().find((item) => item.isSelected);\n    }\n}\n\nexport const accountMoveStatusBarSecuredField = {\n    ...statusBarField,\n    component: AccountMoveStatusBarSecuredField,\n    displayName: _t(\"Status with secured indicator for Journal Entries\"),\n    supportedTypes: [\"state\"],\n    additionalClasses: [\"o_field_statusbar\"],\n};\n\nregistry.category(\"fields\").add(\"account_move_statusbar_secured\", accountMoveStatusBarSecuredField);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { SelectionField, selectionField } from \"@web/views/fields/selection/selection_field\";\n\nexport class AccountTypeSelection extends SelectionField {\n    static template = \"account.AccountTypeSelection\";\n    get hierarchyOptions() {\n        const opts = this.options;\n        return [\n            { name: _t('Balance Sheet') },\n            { name: _t('Assets'), children: opts.filter(x => x[0] && x[0].startsWith('asset')) },\n            { name: _t('Liabilities'), children: opts.filter(x => x[0] && x[0].startsWith('liability')) },\n            { name: _t('Equity'), children: opts.filter(x => x[0] && x[0].startsWith('equity')) },\n            { name: _t('Profit & Loss') },\n            { name: _t('Income'), children: opts.filter(x => x[0] && x[0].startsWith('income')) },\n            { name: _t('Expense'), children: opts.filter(x => x[0] && x[0].startsWith('expense')) },\n            { name: _t('Other'), children: opts.filter(x => x[0] && x[0] === 'off_balance') },\n        ];\n    }\n}\n\nexport const accountTypeSelection = {\n    ...selectionField,\n    component: AccountTypeSelection,\n};\n\nregistry.category(\"fields\").add(\"account_type_selection\", accountTypeSelection);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { Component } from \"@odoo/owl\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nconst WARNING_TYPE_ORDER = [\"danger\", \"warning\", \"info\"];\n\nexport class ActionableErrors extends Component {\n    static props = { errorData: {type: Object} };\n    static template = \"account.ActionableErrors\";\n\n    setup() {\n        super.setup();\n        this.actionService = useService(\"action\");\n    }\n\n    get errorData() {\n        return this.props.errorData;\n    }\n\n    async handleOnClick(errorData){\n        if (errorData.action?.view_mode) {\n            // view_mode is not handled JS side\n            errorData.action['views'] = errorData.action.view_mode.split(',').map(mode => [false, mode]);\n            delete errorData.action['view_mode'];\n        }\n        this.env.model.action.doAction(errorData.action);\n    }\n\n    get sortedActionableErrors() {\n        return this.errorData && Object.fromEntries(\n            Object.entries(this.errorData).sort(\n                (a, b) =>\n                    WARNING_TYPE_ORDER.indexOf(a[1][\"level\"] || \"warning\") -\n                    WARNING_TYPE_ORDER.indexOf(b[1][\"level\"] || \"warning\"),\n            ),\n        );\n    }\n}\n\nexport class ActionableErrorsField extends ActionableErrors {\n    static props = { ...standardFieldProps };\n\n    get errorData() {\n        return this.props.record.data[this.props.name];\n    }\n}\n\nexport const actionableErrorsField = {component: ActionableErrorsField};\nregistry.category(\"fields\").add(\"actionable_errors\", actionableErrorsField);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { X2ManyField, x2ManyField } from \"@web/views/fields/x2many/x2many_field\";\n\n\nexport class AutoSaveResPartnerField extends X2ManyField {\n     async onAdd({ context, editable } = {}) {\n        await this.props.record.model.root.save();\n        await super.onAdd({ context, editable });\n     }\n}\n\nexport const autoSaveResPartnerField = {\n    ...x2ManyField,\n    component: AutoSaveResPartnerField,\n};\n\nregistry.category(\"fields\").add(\"auto_save_res_partner\", autoSaveResPartnerField);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\nimport { Many2ManyTagsField, many2ManyTagsField } from \"@web/views/fields/many2many_tags/many2many_tags_field\";\n\nexport class AutosaveMany2ManyTagsField extends Many2ManyTagsField {\n    setup() {\n        super.setup();\n\n        this.lastBalance = this.props.record.data.balance;\n        this.lastAccount = this.props.record.data.account_id;\n        this.lastPartner = this.props.record.data.partner_id;\n\n        const super_update = this.update;\n        this.update = (recordlist) => {\n            super_update(recordlist);\n            this._saveOnUpdate();\n        };\n        useRecordObserver(this.onRecordChange.bind(this));\n    }\n\n    async deleteTag(id) {\n        await super.deleteTag(id);\n        await this._saveOnUpdate();\n    }\n\n    onRecordChange(record) {\n        const line = record.data;\n        if (line.tax_ids.records.length > 0) {\n            if (line.balance !== this.lastBalance\n                || line.account_id[0] !== this.lastAccount[0]\n                || line.partner_id[0] !== this.lastPartner[0]) {\n                this.lastBalance = line.balance;\n                this.lastAccount = line.account_id;\n                this.lastPartner = line.partner_id;\n                return record.model.root.save();\n            }\n        }\n    }\n\n    async _saveOnUpdate() {\n        await this.props.record.model.root.save();\n    }\n}\n\nexport const autosaveMany2ManyTagsField = {\n    ...many2ManyTagsField,\n    component: AutosaveMany2ManyTagsField,\n};\n\nregistry.category(\"fields\").add(\"autosave_many2many_tags\", autosaveMany2ManyTagsField);\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { DocumentFileUploader } from \"../document_file_uploader/document_file_uploader\";\n\nimport { Component, onWillStart } from \"@odoo/owl\";\n\nexport class BillGuide extends Component {\n    static template = \"account.BillGuide\";\n    static components = {\n        DocumentFileUploader,\n    };\n    static props = [\"*\"];  // could contain view_widget props\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n        this.context = null;\n        this.alias = null;\n        onWillStart(this.onWillStart);\n    }\n\n    async onWillStart() {\n        const rec = this.props.record;\n        const ctx = this.env.searchModel.context;\n        if (rec) {\n            // prepare context from journal record\n            this.context = {\n                default_journal_id: rec.resId,\n                default_move_type: (rec.data.type === 'sale' && 'out_invoice') || (rec.data.type === 'purchase' && 'in_invoice') || 'entry',\n                active_model: rec.resModel,\n                active_ids: [rec.resId],\n            }\n            this.alias = rec.data.alias_domain_id && rec.data.alias_id[1] || false;\n        } else if (!ctx?.default_journal_id && ctx?.active_id) {\n            this.context = {\n                default_journal_id: ctx.active_id,\n            }\n        }\n    }\n\n    handleButtonClick(action, model=\"account.journal\") {\n        this.action.doActionButton({\n            resModel: model,\n            name: action,\n            context: this.context || this.env.searchModel.context,\n            type: 'object',\n        });\n    }\n}\n\n\nexport const billGuide = {\n    component: BillGuide,\n};\n\nregistry.category(\"view_widgets\").add(\"bill_upload_guide\", billGuide);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { CharField, charField } from \"@web/views/fields/char/char_field\";\n\n// Ensure that in Hoot tests, this module is loaded after `@mail/js/onchange_on_keydown`\n// (needed because that module patches `charField`).\nimport \"@mail/js/onchange_on_keydown\";\n\nexport class CharWithPlaceholderField extends CharField {\n    static template = \"account.CharWithPlaceholderField\";\n\n    /** Override **/\n    get formattedValue() {\n        return super.formattedValue || this.placeholder;\n    }\n}\n\nexport const charWithPlaceholderField = {\n    ...charField,\n    component: CharWithPlaceholderField,\n};\n\nregistry.category(\"fields\").add(\"char_with_placeholder_field\", charWithPlaceholderField);\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { FileUploader } from \"@web/views/fields/file_handler\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\n\nimport { Component, markup } from \"@odoo/owl\";\n\nexport class DocumentFileUploader extends Component {\n    static template = \"account.DocumentFileUploader\";\n    static components = {\n        FileUploader,\n    };\n    static props = {\n        ...standardWidgetProps,\n        record: { type: Object, optional: true },\n        slots: { type: Object, optional: true },\n        resModel: { type: String, optional: true },\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n        this.notification = useService(\"notification\");\n        this.attachmentIdsToProcess = [];\n        this.extraContext = this.getExtraContext();\n    }\n\n    // To pass extra context while creating record\n    getExtraContext() {\n        return {};\n    }\n\n    async onFileUploaded(file) {\n        const att_data = {\n            name: file.name,\n            mimetype: file.type,\n            datas: file.data,\n        };\n        const [att_id] = await this.orm.create(\"ir.attachment\", [att_data], {\n            context: { ...this.extraContext, ...this.env.searchModel.context },\n        });\n        this.attachmentIdsToProcess.push(att_id);\n    }\n\n    // To define specific resModal from another model\n    getResModel() {\n        return this.props.resModel;\n    }\n\n    async onUploadComplete() {\n        const resModal = this.getResModel();\n        let action;\n        try {\n            action = await this.orm.call(\n                resModal,\n                \"create_document_from_attachment\",\n                [\"\", this.attachmentIdsToProcess],\n                { context: { ...this.extraContext, ...this.env.searchModel.context } }\n            );\n        } finally {\n            // ensures attachments are cleared on success as well as on error\n            this.attachmentIdsToProcess = [];\n        }\n        if (action.context && action.context.notifications) {\n            for (const [file, msg] of Object.entries(action.context.notifications)) {\n                this.notification.add(msg, {\n                    title: file,\n                    type: \"info\",\n                    sticky: true,\n                });\n            }\n            delete action.context.notifications;\n        }\n        if (action.help?.length) {\n            action.help = markup(action.help);\n        }\n        this.action.doAction(action);\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { SelectionField, selectionField } from \"@web/views/fields/selection/selection_field\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class DocumentStatePopover extends Component {\n    static template = \"account.DocumentStatePopover\";\n    static props = {\n        close: Function,\n        onClose: Function,\n        copyText: Function,\n        message: String,\n    };\n}\n\nexport class DocumentState extends SelectionField {\n    static template = \"account.DocumentState\";\n\n    setup() {\n        super.setup();\n        this.popover = useService(\"popover\");\n        this.notification = useService(\"notification\");\n    }\n\n    get message() {\n        return this.props.record.data.message;\n    }\n\n    copyText() {\n        navigator.clipboard.writeText(this.message);\n        this.notification.add(_t(\"Text copied\"), { type: \"success\" });\n        this.popoverCloseFn();\n        this.popoverCloseFn = null;\n    }\n\n    showMessagePopover(ev) {\n        const close = () => {\n            this.popoverCloseFn();\n            this.popoverCloseFn = null;\n        };\n\n        if (this.popoverCloseFn) {\n            close();\n            return;\n        }\n\n        this.popoverCloseFn = this.popover.add(\n            ev.currentTarget,\n            DocumentStatePopover,\n            {\n                message: this.message,\n                copyText: this.copyText.bind(this),\n                onClose: close,\n            },\n            {\n                closeOnClickAway: true,\n                position: \"top\",\n            },\n        );\n    }\n}\n\nregistry.category(\"fields\").add(\"account_document_state\", {\n    ...selectionField,\n    component: DocumentState,\n});\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { SelectionField, selectionField } from \"@web/views/fields/selection/selection_field\";\n\nexport class DynamicSelectionField extends SelectionField {\n\n    static props = {\n        ...SelectionField.props,\n        available_field: { type: String },\n    }\n\n    get availableOptions() {\n        return this.props.record.data[this.props.available_field]?.split(\",\") || [];\n    }\n\n    /**\n     * Filter the options with the accepted available options.\n     * @override\n     */\n    get options() {\n        const availableOptions = this.availableOptions;\n        return super.options.filter(x => availableOptions.includes(x[0]));\n    }\n\n    /**\n     * In dynamic selection field, sometimes we can have no options available.\n     * This override handles that case by adding optional chaining when accessing the found options.\n     * @override\n     */\n    get string() {\n        if (this.type === \"selection\") {\n            return this.props.record.data[this.props.name] !== false\n                ? this.options.find((o) => o[0] === this.props.record.data[this.props.name])?.[1]\n                : \"\";\n        }\n        return super.string;\n    }\n\n}\n\n/*\nEXAMPLE USAGE:\n\nIn python:\nthe_available_field = fields.Char()  # string of comma separated available selection field keys\nthe_selection_field = fields.Selection([ ... ])\n\nIn the views:\n<field name=\"the_available_field\" column_invisible=\"1\"/>\n<field name=\"the_selection_field\"\n       widget=\"dynamic_selection\"\n       options=\"{'available_field': 'the_available_field'}\"/>\n */\n\nregistry.category(\"fields\").add(\"dynamic_selection\", {\n    ...selectionField,\n    component: DynamicSelectionField,\n    extractProps: (fieldInfo, dynamicInfo) => ({\n        ...selectionField.extractProps(fieldInfo, dynamicInfo),\n        available_field: fieldInfo.options.available_field,\n    }),\n})\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { Component } from \"@odoo/owl\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nclass ListItem extends Component {\n    static template = \"account.GroupedItemTemplate\";\n    static props = [\"item_vals\", \"options\"];\n}\n\nclass ListGroup extends Component {\n    static template = \"account.GroupedItemsTemplate\";\n    static components = { ListItem };\n    static props = [\"group_vals\", \"options\"];\n}\n\nclass ShowGroupedList extends Component {\n    static template = \"account.GroupedListTemplate\";\n    static components = { ListGroup };\n    static props = {...standardFieldProps};\n    getValue() {\n        const value = this.props.record.data[this.props.name];\n        return value\n            ? JSON.parse(value)\n            : { groups_vals: [], options: { discarded_number: \"\", columns: [] } };\n    }\n}\n\nregistry.category(\"fields\").add(\"grouped_view_widget\", {\n    component: ShowGroupedList,\n});\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { Component } from \"@odoo/owl\";\n\nexport class JournalDashboardActivity extends Component {\n    static template = \"account.JournalDashboardActivity\";\n    static props = { ...standardFieldProps };\n\n    setup() {\n        this.action = useService(\"action\");\n        this.MAX_ACTIVITY_DISPLAY = 5;\n        this.formatData(this.props);\n    }\n\n    formatData(props) {\n        this.info = JSON.parse(this.props.record.data[this.props.name]);\n        this.info.more_activities = false;\n        if (this.info.activities.length > this.MAX_ACTIVITY_DISPLAY) {\n            this.info.more_activities = true;\n            this.info.activities = this.info.activities.slice(0, this.MAX_ACTIVITY_DISPLAY);\n        }\n    }\n\n    async openActivity(activity) {\n        this.action.doAction({\n            type: 'ir.actions.act_window',\n            name: _t('Journal Entry'),\n            target: 'current',\n            res_id: activity.res_id,\n            res_model: 'account.move',\n            views: [[false, 'form']],\n        });\n    }\n\n    openAllActivities(e) {\n        this.action.doAction({\n            type: 'ir.actions.act_window',\n            name: _t('Journal Entries'),\n            res_model: 'account.move',\n            views: [[false, 'kanban'], [false, 'form']],\n            search_view_id: [false],\n            domain: [['journal_id', '=', this.props.record.resId], ['activity_ids', '!=', false]],\n        });\n    }\n}\n\nexport const journalDashboardActivity = {\n    component: JournalDashboardActivity,\n};\n\nregistry.category(\"fields\").add(\"kanban_vat_activity\", journalDashboardActivity);\n", "import { Component, useState } from \"@odoo/owl\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport {debounce} from \"@web/core/utils/timing\";\n\n\nexport class JsonCheckboxes extends Component {\n    static template = \"account.JsonCheckboxes\";\n    static components = { CheckBox };\n    static props = {\n        ...standardFieldProps,\n    };\n\n    setup() {\n        super.setup();\n        this.checkboxes = useState(this.props.record.data[this.props.name]);\n        this.debouncedCommitChanges = debounce(this.commitChanges.bind(this), 100);\n    }\n\n    commitChanges() {\n        this.props.record.update({ [this.props.name]: this.checkboxes });\n    }\n\n    onChange(key, checked) {\n        this.checkboxes[key]['checked'] = checked;\n        this.debouncedCommitChanges();\n    }\n\n}\n\nexport const jsonCheckboxes = {\n    component: JsonCheckboxes,\n    supportedTypes: [\"jsonb\"],\n}\n\nregistry.category(\"fields\").add(\"account_json_checkboxes\", jsonCheckboxes);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { FileInput } from \"@web/core/file_input/file_input\";\nimport { Component, onWillUnmount } from \"@odoo/owl\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nexport class MailAttachments extends Component {\n    static template = \"account.mail_attachments\";\n    static components = { FileInput };\n    static props = {...standardFieldProps};\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n        this.notification = useService(\"notification\");\n        this.attachmentIdsToUnlink = new Set();\n\n        onWillUnmount(this.onWillUnmount);\n    }\n\n    getValue(){\n        return this.props.record.data[this.props.name] || [];\n    }\n\n    getUrl(attachmentId) {\n        return `/web/content/${attachmentId}?download=true`\n    }\n\n    getExtension(file) {\n        return file.name.replace(/^.*\\./, \"\");\n    }\n\n    onFileUploaded(files) {\n        let extraFiles = [];\n        for (const file of files) {\n            if (file.error) {\n                return this.notification.add(file.error, {\n                    title: _t(\"Uploading error\"),\n                    type: \"danger\",\n                });\n            }\n\n            extraFiles.push({\n                id: file.id,\n                name: file.filename,\n                mimetype: file.mimetype,\n                placeholder: false,\n                manual: true,\n            });\n        }\n        this.props.record.update({ [this.props.name]: this.getValue().concat(extraFiles) });\n    }\n\n    onFileRemove(deleteId) {\n        const newValue = [];\n        for (let item of this.getValue()) {\n            if (item.id === deleteId) {\n                if (item.placeholder || item.protect_from_deletion) {\n                    const copyItem = Object.assign({ skip: true }, item);\n                    newValue.push(copyItem);\n                } else {\n                    this.attachmentIdsToUnlink.add(item.id);\n                }\n            } else {\n                newValue.push(item);\n            }\n        }\n        this.props.record.update({ [this.props.name]: newValue });\n    }\n\n    async onWillUnmount(){\n        // Unlink added attachments if the wizard is not saved.\n        if(!this.props.record.resId){\n            this.getValue().forEach((item) => {\n                if(item.manual){\n                    this.attachmentIdsToUnlink.add(item.id);\n                }\n            });\n        }\n        if(this.attachmentIdsToUnlink.size > 0){\n            await this.orm.unlink(\"ir.attachment\", Array.from(this.attachmentIdsToUnlink));\n        }\n    }\n}\n\nexport const mailAttachments = {\n    component: MailAttachments,\n};\n\nregistry.category(\"fields\").add(\"mail_attachments\", mailAttachments);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nimport { Many2XAutocomplete } from \"@web/views/fields/relational_utils\";\nimport {\n    Many2ManyTagsField,\n    many2ManyTagsField,\n} from \"@web/views/fields/many2many_tags/many2many_tags_field\";\n\nimport { TaxAutoComplete } from \"@account/components/tax_autocomplete/tax_autocomplete\";\n\nexport class Many2ManyTaxTagsAutocomplete extends Many2XAutocomplete {\n    static components = {\n        ...Many2XAutocomplete.components,\n        AutoComplete: TaxAutoComplete,\n    };\n    get SearchMoreButtonLabel() {\n        return _t(\"Not sure... Help me!\");\n    }\n\n    search(name) {\n        return this.orm\n            .call(this.props.resModel, \"search_read\", [], {\n                domain: [...this.props.getDomain(), [\"name\", \"ilike\", name]],\n                fields: [\"id\", \"name\", \"tax_scope\"],\n            })\n            .then((records) => {\n                return this.orm\n                    .call(\"account.tax\", \"fields_get\", [], { attributes: [\"selection\"] })\n                    .then((fields) => {\n                        const selectionOptions = fields.tax_scope.selection;\n\n                        const recordsWithLabels = records.map((record) => {\n                            const selectedOption = selectionOptions.find(\n                                (option) => option[0] === record.tax_scope\n                            );\n                            const label = selectedOption ? selectedOption[1] : undefined;\n                            return { ...record, tax_scope: label };\n                        });\n\n                        return recordsWithLabels;\n                    });\n            });\n    }\n\n    mapRecordToOption(result) {\n        return {\n            value: result.id,\n            label: result.name ? result.name.split(\"\\n\")[0] : _t(\"Unnamed\"),\n            displayName: result.name,\n            tax_scope: result.tax_scope,\n        };\n    }\n}\n\nexport class Many2ManyTaxTagsField extends Many2ManyTagsField {\n    static components = {\n        ...Many2ManyTagsField.components,\n        Many2XAutocomplete: Many2ManyTaxTagsAutocomplete,\n    };\n}\n\nexport const many2ManyTaxTagsField = {\n    ...many2ManyTagsField,\n    component: Many2ManyTaxTagsField,\n    additionalClasses: ['o_field_many2many_tags']\n};\n\nregistry.category(\"fields\").add(\"many2many_tax_tags\", many2ManyTaxTagsField);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { Component } from \"@odoo/owl\";\n\nclass AccountOnboardingWidget extends Component {\n    static template = \"account.Onboarding\";\n    static props = {\n        ...standardWidgetProps,\n    };\n    setup() {\n        this.action = useService(\"action\");\n        this.orm = useService(\"orm\");\n    }\n\n    get recordOnboardingSteps() {\n        return JSON.parse(this.props.record.data.kanban_dashboard).onboarding?.steps;\n    }\n\n    async onboardingLinkClicked(step) {\n        const action = await this.orm.call(\"onboarding.onboarding.step\", step.action, [], {\n            context: {\n                journal_id: this.props.record.resId,\n            }\n        });\n        this.action.doAction(action);\n    }\n}\n\nexport const accountOnboarding = {\n    component: AccountOnboardingWidget,\n}\n\nregistry.category(\"view_widgets\").add(\"account_onboarding\", accountOnboarding);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { Many2OneField, many2OneField } from \"@web/views/fields/many2one/many2one_field\";\n\nclass LineOpenMoveWidget extends Many2OneField {\n    async openAction() {\n        this.action.doActionButton({\n            type: \"object\",\n            resId: this.props.record.data[this.props.name][0],\n            name: \"action_open_business_doc\",\n            resModel: \"account.move.line\",\n        });\n    }\n}\n\nexport const lineOpenMoveWidget = {\n    ...many2OneField,\n    component: LineOpenMoveWidget,\n};\n\nregistry.category(\"fields\").add(\"line_open_move_widget\", lineOpenMoveWidget);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { Component } from \"@odoo/owl\";\n\nclass OpenMoveWidget extends Component {\n    static template = \"account.OpenMoveWidget\";\n    static props = { ...standardFieldProps };\n\n    setup() {\n        super.setup();\n        this.action = useService(\"action\");\n    }\n\n    async openMove(ev) {\n        this.action.doActionButton({\n            type: \"object\",\n            resId: this.props.record.resId,\n            name: \"action_open_business_doc\",\n            resModel: this.props.record.resModel,\n        });\n    }\n}\n\nregistry.category(\"fields\").add(\"open_move_widget\", {\n    component: OpenMoveWidget,\n});\n", "/** @odoo-module */\nimport { ProductCatalogOrderLine } from \"@product/product_catalog/order_line/order_line\";\n\nexport class ProductCatalogAccountMoveLine extends ProductCatalogOrderLine {\n    static props = {\n        ...ProductCatalogOrderLine.props,\n        min_qty: { type: Number, optional: true },\n    };\n}\n", "/** @odoo-module */\nimport { ProductCatalogKanbanController } from \"@product/product_catalog/kanban_controller\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { _t } from \"@web/core/l10n/translation\";\n\npatch(ProductCatalogKanbanController.prototype, {\n    async _defineButtonContent() {\n        const fields = this.orderResModel === \"account.move\" ? [\"state\", \"move_type\"] : [\"state\"];\n        const orderStateInfo = await this.orm.searchRead(\n            this.orderResModel,\n            [[\"id\", \"=\", this.orderId]],\n            fields,\n        );\n        if (orderStateInfo[0]?.move_type === \"out_invoice\") {\n            this.buttonString = _t(\"Back to Invoice\");\n        } else if (orderStateInfo[0]?.move_type === \"in_invoice\") {\n            this.buttonString = _t(\"Back to Bill\");\n        } else {\n            this.buttonString = super._defineButtonContent();\n        }\n    },\n});\n", "/** @odoo-module */\nimport { ProductCatalogKanbanRecord } from \"@product/product_catalog/kanban_record\";\nimport { ProductCatalogAccountMoveLine } from \"./account_move_line\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(ProductCatalogKanbanRecord.prototype, {\n    get orderLineComponent() {\n        if (this.env.orderResModel === \"account.move\") {\n            return ProductCatalogAccountMoveLine;\n        }\n        return super.orderLineComponent;\n    },\n\n    addProduct() {\n        if (this.productCatalogData.quantity === 0 && this.productCatalogData.min_qty) {\n            super.addProduct(this.productCatalogData.min_qty);\n        } else {\n            super.addProduct(...arguments);\n        }\n    },\n})\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { AutoComplete } from \"@web/core/autocomplete/autocomplete\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { Many2XAutocomplete } from \"@web/views/fields/relational_utils\";\nimport { Many2OneField, many2OneField } from \"@web/views/fields/many2one/many2one_field\";\nimport { onMounted, onPatched, onWillUnmount, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport {\n    SectionAndNoteListRenderer,\n    sectionAndNoteFieldOne2Many,\n} from \"@account/components/section_and_note_fields_backend/section_and_note_fields_backend\";\nimport { useProductAndLabelAutoresize } from \"@account/core/utils/product_and_label_autoresize\";\nimport { X2ManyField, x2ManyField } from \"@web/views/fields/x2many/x2many_field\";\n\nexport class ProductLabelSectionAndNoteListRender extends SectionAndNoteListRenderer {\n\n    setup() {\n        super.setup();\n        this.productColumns = [\"product_id\", \"product_template_id\"];\n    }\n\n    getCellTitle(column, record) {\n        // When using this list renderer, we don't want the product_id cell to have a tooltip with its label.\n        if (this.productColumns.includes(column.name)) {\n            return;\n        }\n        super.getCellTitle(column, record);\n    }\n\n    getActiveColumns(list) {\n        let activeColumns = super.getActiveColumns(list);\n        const productCol = activeColumns.find((col) => this.productColumns.includes(col.name));\n        const labelCol = activeColumns.find((col) => col.name === \"name\");\n\n        if (productCol) {\n            if (labelCol) {\n                list.records.forEach((record) => (record.columnIsProductAndLabel = true));\n            } else {\n                list.records.forEach((record) => (record.columnIsProductAndLabel = false));\n            }\n            activeColumns = activeColumns.filter((col) => col.name !== \"name\");\n            this.titleField = productCol.name;\n        } else {\n            this.titleField = \"name\";\n        }\n\n        return activeColumns;\n    }\n}\n\nexport class ProductLabelSectionAndNoteOne2Many extends X2ManyField {\n    static components = {\n        ...X2ManyField.components,\n        ListRenderer: ProductLabelSectionAndNoteListRender,\n    };\n}\n\nexport const productLabelSectionAndNoteOne2Many = {\n    ...x2ManyField,\n    component: ProductLabelSectionAndNoteOne2Many,\n    additionalClasses: sectionAndNoteFieldOne2Many.additionalClasses,\n};\n\nregistry\n    .category(\"fields\")\n    .add(\"product_label_section_and_note_field_o2m\", productLabelSectionAndNoteOne2Many);\n\nexport class ProductLabelSectionAndNoteAutocomplete extends AutoComplete {\n    setup() {\n        super.setup();\n        this.labelTextarea = useRef(\"labelNodeRef\");\n    }\n    onInputKeydown(event) {\n        super.onInputKeydown(event);\n        const hotkey = getActiveHotkey(event);\n        const labelVisibilityButton = document.getElementById('labelVisibilityButtonId');\n        if (hotkey === \"enter\") {\n            if (labelVisibilityButton && !this.labelTextarea.el) {\n                labelVisibilityButton.click();\n                event.stopPropagation();\n                event.preventDefault();\n            }\n        }\n    }\n}\n\nexport class ProductLabelSectionAndNoteFieldAutocomplete extends Many2XAutocomplete {\n    static components = {\n        ...Many2XAutocomplete.components,\n        AutoComplete: ProductLabelSectionAndNoteAutocomplete,\n    };\n    static props = {\n        ...Many2XAutocomplete.props,\n        isNote: { type: Boolean },\n        isSection: { type: Boolean },\n        onFocusout: { type: Function, optional: true },\n        updateLabel: { type: Function, optional: true },\n    };\n    static template = \"account.ProductLabelSectionAndNoteFieldAutocomplete\";\n    setup() {\n        super.setup();\n        this.input = useRef(\"section_and_note_input\");\n    }\n\n    get isSectionOrNote() {\n        return this.props.isSection || this.props.isNote;\n    }\n\n    get isSection() {\n        return this.props.isSection;\n    }\n}\n\nexport class ProductLabelSectionAndNoteField extends Many2OneField {\n    static components = {\n        ...Many2OneField.components,\n        Many2XAutocomplete: ProductLabelSectionAndNoteFieldAutocomplete,\n    };\n    static template = \"account.ProductLabelSectionAndNoteField\";\n\n    setup() {\n        super.setup();\n        this.isPrintMode = useState({ value: false });\n        this.labelVisibility = useState({ value: false });\n        this.switchToLabel = false;\n        this.columnIsProductAndLabel = useState({ value: this.props.record.columnIsProductAndLabel });\n        this.labelNode = useRef(\"labelNodeRef\");\n        useProductAndLabelAutoresize(this.labelNode, { targetParentName: this.props.name });\n        this.productNode = useRef(\"productNodeRef\");\n        useProductAndLabelAutoresize(this.productNode, { targetParentName: this.props.name });\n\n        useEffect(\n            () => {\n                this.columnIsProductAndLabel.value = this.props.record.columnIsProductAndLabel;\n            },\n            () => [this.props.record.columnIsProductAndLabel]\n        );\n\n        onPatched(() => {\n            if (this.labelNode.el && this.switchToLabel) {\n                this.switchToLabel = false;\n                this.labelNode.el.focus();\n            }\n        });\n\n        this.onBeforePrint = () => {\n            this.isPrintMode.value = true;\n        };\n\n        this.onAfterPrint = () => {\n            this.isPrintMode.value = false;\n        };\n\n        // The following hooks are used to make a div visible only in the print view. This div is necessary in the\n        // print view in order not to have scroll bars but can't be displayed in the normal view because it adds\n        // an empty line. This is done by switching an attribute to true only during the print view life cycle and\n        // including the said div in a t-if depending on that attribute.\n        onMounted(() => {\n            window.addEventListener(\"beforeprint\", this.onBeforePrint);\n            window.addEventListener(\"afterprint\", this.onAfterPrint);\n        });\n\n        onWillUnmount(() => {\n            window.removeEventListener(\"beforeprint\", this.onBeforePrint);\n            window.removeEventListener(\"afterprint\", this.onAfterPrint);\n        });\n    }\n\n    get productName() {\n        return this.props.record.data[this.props.name][1];\n    }\n\n    get label() {\n        let label = this.props.record.data.name;\n        if (label.includes(this.productName)) {\n            label = label.replace(this.productName, \"\");\n            if (label.includes(\"\\n\")) {\n                label = label.replace(\"\\n\", \"\");\n            }\n        }\n        return label;\n    }\n\n    get Many2XAutocompleteProps() {\n        const props = super.Many2XAutocompleteProps;\n        props.isSection = this.isSection(this.props.record);\n        props.isNote = this.isNote(this.props.record);\n        props.placeholder = _t(\"Search a product\");\n        props.updateLabel = this.updateLabel.bind(this);\n        return props;\n    }\n\n    get isProductClickable() {\n        return this.props.record.evalContext.parent.state !== \"draft\";\n    }\n\n    get isSectionOrNote() {\n        return this.isSection(this.props.record) || this.isNote(this.props.record);\n    }\n\n    get sectionAndNoteClasses() {\n        if (this.isSection()) {\n            return \"fw-bold\";\n        } else if (this.isNote()) {\n            return \"fst-italic\";\n        }\n        return \"\";\n    }\n\n    isSection(record = null) {\n        record = record || this.props.record;\n        return record.data.display_type === \"line_section\";\n    }\n\n    isNote(record = null) {\n        record = record || this.props.record;\n        return record.data.display_type === \"line_note\";\n    }\n\n    switchLabelVisibility() {\n        this.labelVisibility.value = !this.labelVisibility.value;\n        this.switchToLabel = true;\n    }\n\n    updateLabel(value) {\n        this.props.record.update({\n            name: (\n                this.productName && value && this.productName.concat(\"\\n\", value)\n                || !value && this.productName\n                || value\n            ),\n        });\n    }\n}\n\nexport const productLabelSectionAndNoteField = {\n    ...many2OneField,\n    listViewWidth: [240, 400],\n    component: ProductLabelSectionAndNoteField,\n};\nregistry\n    .category(\"fields\")\n    .add(\"product_label_section_and_note_field\", productLabelSectionAndNoteField);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { X2ManyField, x2ManyField } from \"@web/views/fields/x2many/x2many_field\";\nimport { TextField, ListTextField } from \"@web/views/fields/text/text_field\";\nimport { CharField } from \"@web/views/fields/char/char_field\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { Component, useEffect } from \"@odoo/owl\";\n\nexport class SectionAndNoteListRenderer extends ListRenderer {\n    static template = \"account.sectionAndNoteListRenderer\";\n\n    /**\n     * The purpose of this extension is to allow sections and notes in the one2many list\n     * primarily used on Sales Orders and Invoices\n     *\n     * @override\n     */\n    setup() {\n        super.setup();\n        this.titleField = \"name\";\n        useEffect(\n            (editedRecord) => this.focusToName(editedRecord),\n            () => [this.editedRecord]\n        )\n    }\n\n    focusToName(editRec) {\n        if (editRec && editRec.isNew && this.isSectionOrNote(editRec)) {\n            const col = this.columns.find((c) => c.name === this.titleField);\n            this.focusCell(col, null);\n        }\n    }\n\n    isSectionOrNote(record=null) {\n        record = record || this.record;\n        return ['line_section', 'line_note'].includes(record.data.display_type);\n    }\n\n    getRowClass(record) {\n        const existingClasses = super.getRowClass(record);\n        return `${existingClasses} o_is_${record.data.display_type}`;\n    }\n\n    getCellClass(column, record) {\n        const classNames = super.getCellClass(column, record);\n        if (this.isSectionOrNote(record) && column.widget !== \"handle\" && column.name !== this.titleField) {\n            return `${classNames} o_hidden`;\n        }\n        return classNames;\n    }\n\n    getColumns(record) {\n        const columns = super.getColumns(record);\n        if (this.isSectionOrNote(record)) {\n            return this.getSectionColumns(columns);\n        }\n        return columns;\n    }\n\n    getSectionColumns(columns) {\n        const sectionCols = columns.filter((col) => col.widget === \"handle\" || col.type === \"field\" && col.name === this.titleField);\n        return sectionCols.map((col) => {\n            if (col.name === this.titleField) {\n                return { ...col, colspan: columns.length - sectionCols.length + 1 };\n            } else {\n                return { ...col };\n            }\n        });\n    }\n}\n\nexport class SectionAndNoteFieldOne2Many extends X2ManyField {\n    static components = {\n        ...X2ManyField.components,\n        ListRenderer: SectionAndNoteListRenderer,\n    };\n}\n\nexport class SectionAndNoteText extends Component {\n    static template = \"account.SectionAndNoteText\";\n    static props = { ...standardFieldProps };\n\n    get componentToUse() {\n        return this.props.record.data.display_type === 'line_section' ? CharField : TextField;\n    }\n}\n\nexport class ListSectionAndNoteText extends SectionAndNoteText {\n    get componentToUse() {\n        return this.props.record.data.display_type !== \"line_section\"\n            ? ListTextField\n            : super.componentToUse;\n    }\n}\n\nexport const sectionAndNoteFieldOne2Many = {\n    ...x2ManyField,\n    component: SectionAndNoteFieldOne2Many,\n    additionalClasses: [...x2ManyField.additionalClasses || [], \"o_field_one2many\"],\n};\n\nexport const sectionAndNoteText = {\n    component: SectionAndNoteText,\n    additionalClasses: [\"o_field_text\"],\n};\n\nexport const listSectionAndNoteText = {\n    ...sectionAndNoteText,\n    component: ListSectionAndNoteText,\n};\n\nregistry.category(\"fields\").add(\"section_and_note_one2many\", sectionAndNoteFieldOne2Many);\nregistry.category(\"fields\").add(\"section_and_note_text\", sectionAndNoteText);\nregistry.category(\"fields\").add(\"list.section_and_note_text\", listSectionAndNoteText);\n", "import { AutoComplete } from \"@web/core/autocomplete/autocomplete\";\n\nexport class TaxAutoComplete extends AutoComplete {\n    static template = \"account.TaxAutoComplete\";\n}\n", "/** @odoo-module **/\n\nimport { formatMonetary } from \"@web/views/fields/formatters\";\nimport { formatFloat } from \"@web/core/utils/numbers\";\nimport { parseFloat } from \"@web/views/fields/parsers\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { registry } from \"@web/core/registry\";\nimport {\n    Component,\n    onPatched,\n    onWillUpdateProps,\n    onWillRender,\n    toRaw,\n    useRef,\n    useState,\n} from \"@odoo/owl\";\nimport { useNumpadDecimal } from \"@web/views/fields/numpad_decimal_hook\";\n\n/**\n A line of some TaxTotalsComponent, giving the values of a tax group.\n **/\nclass TaxGroupComponent extends Component {\n    static props = {\n        totals: { optional: true },\n        subtotal: { optional: true },\n        taxGroup: { optional: true },\n        onChangeTaxGroup: { optional: true },\n        isReadonly: Boolean,\n        invalidate: Function,\n    };\n    static template = \"account.TaxGroupComponent\";\n\n    setup() {\n        this.inputTax = useRef(\"taxValueInput\");\n        this.state = useState({ value: \"readonly\" });\n        onPatched(() => {\n            if (this.state.value === \"edit\") {\n                const { taxGroup } = this.props;\n                const newVal = formatFloat(taxGroup.tax_amount_currency, { digits: this.props.totals.currency_pd });\n                this.inputTax.el.value = newVal;\n                this.inputTax.el.focus(); // Focus the input\n            }\n        });\n        onWillUpdateProps(() => {\n            this.setState(\"readonly\");\n        });\n        useNumpadDecimal();\n    }\n\n    formatMonetary(value) {\n        return formatMonetary(value, {currencyId: this.props.totals.currency_id});\n    }\n\n    //--------------------------------------------------------------------------\n    // Main methods\n    //--------------------------------------------------------------------------\n\n    /**\n     * The purpose of this method is to change the state of the component.\n     * It can have one of the following three states:\n     *  - readonly: display in read-only mode of the field,\n     *  - edit: display with a html input field,\n     *  - disable: display with a html input field that is disabled.\n     *\n     * If a value other than one of these 3 states is passed as a parameter,\n     * the component is set to readonly by default.\n     *\n     * @param {String} value\n     */\n    setState(value) {\n        if ([\"readonly\", \"edit\", \"disable\"].includes(value)) {\n            this.state.value = value;\n        }\n        else {\n            this.state.value = \"readonly\";\n        }\n    }\n\n    /**\n     * This method handles the \"_onChangeTaxValue\" event. In this method,\n     * we get the new value for the tax group, we format it and we call\n     * the method to recalculate the tax lines. At the moment the method\n     * is called, we disable the html input field.\n     *\n     * In case the value has not changed or the tax group is equal to 0,\n     * the modification does not take place.\n     */\n    _onChangeTaxValue() {\n        this.setState(\"disable\"); // Disable the input\n        const oldValue = this.props.taxGroup.tax_amount_currency;\n        let newValue;\n        try {\n            newValue = parseFloat(this.inputTax.el.value); // Get the new value\n        } catch {\n            this.inputTax.el.value = oldValue;\n            this.setState(\"edit\");\n            return;\n        }\n        // The newValue can\"t be equals to 0\n        if (newValue === oldValue || newValue === 0) {\n            this.setState(\"readonly\");\n            return;\n        }\n        const deltaValue = newValue - oldValue;\n        this.props.taxGroup.tax_amount_currency += deltaValue;\n        this.props.subtotal.tax_amount_currency += deltaValue;\n        this.props.totals.tax_amount_currency += deltaValue;\n        this.props.totals.total_amount_currency += deltaValue;\n\n        this.props.onChangeTaxGroup({\n            oldValue,\n            newValue: newValue,\n            taxGroupId: this.props.taxGroup.id,\n        });\n    }\n}\n\n/**\n Widget used to display tax totals by tax groups for invoices, PO and SO,\n and possibly allowing editing them.\n\n Note that this widget requires the object it is used on to have a\n currency_id field.\n **/\nexport class TaxTotalsComponent extends Component {\n    static template = \"account.TaxTotalsField\";\n    static components = { TaxGroupComponent };\n    static props = {\n        ...standardFieldProps,\n    };\n\n    setup() {\n        this.totals = {};\n        this.formatData(this.props);\n        onWillRender(() => this.formatData(this.props));\n    }\n\n    get readonly() {\n        return this.props.readonly;\n    }\n\n    invalidate() {\n        return this.props.record.setInvalidField(this.props.name);\n    }\n\n    formatMonetary(value) {\n        return formatMonetary(value, {currencyId: this.totals.currency_id});\n    }\n\n    /**\n     * This method is the main function of the tax group widget.\n     * It is called by the TaxGroupComponent and receives the newer tax value.\n     *\n     * It is responsible for triggering an event to notify the ORM of a change.\n     */\n    _onChangeTaxValueByTaxGroup({ oldValue, newValue }) {\n        if (oldValue === newValue) return;\n        this.props.record.update({ [this.props.name]: this.totals });\n        delete this.totals.cash_rounding_base_amount_currency;\n    }\n\n    formatData(props) {\n        let totals = JSON.parse(JSON.stringify(toRaw(props.record.data[this.props.name])));\n        if (!totals) {\n            return;\n        }\n        this.totals = totals;\n    }\n}\n\nexport const taxTotalsComponent = {\n    component: TaxTotalsComponent,\n};\n\nregistry.category(\"fields\").add(\"account-tax-totals-field\", taxTotalsComponent);\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, soft_checking: params.soft_checking};\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 { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class UploadDropZone extends Component {\n    static template = \"account.UploadDropZone\";\n    static props = {\n        visible: { type: Boolean, optional: true },\n        hideZone: { type: Function, optional: true },\n        dragIcon: { type: String, optional: true },\n        dragText: { type: String, optional: true },\n        dragTitle: { type: String, optional: true },\n    };\n    static defaultProps = {\n        hideZone: () => {},\n    };\n\n    setup() {\n        this.notificationService = useService(\"notification\");\n        this.dashboardState = useState(this.env.dashboardState || {});\n    }\n\n    onDrop(ev) {\n        const selector = '.document_file_uploader.o_input_file';\n        // look for the closest uploader Input as it may have a context\n        let uploadInput = ev.target.closest('.o_drop_area').parentElement.querySelector(selector) || document.querySelector(selector);\n        let files = ev.dataTransfer ? ev.dataTransfer.files : false;\n        if (uploadInput && !!files) {\n            uploadInput.files = ev.dataTransfer.files;\n            uploadInput.dispatchEvent(new Event(\"change\"));\n        } else {\n            this.notificationService.add(\n                _t(\"Could not upload files\"),\n                {\n                    type: \"danger\",\n                });\n        }\n        this.props.hideZone();\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component } from \"@odoo/owl\";\n\nclass X2ManyButtons extends Component {\n    static template = \"account.X2ManyButtons\";\n    static props = {\n        ...standardFieldProps,\n        treeLabel: { type: String },\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n    }\n\n    async openTreeAndDiscard() {\n        const ids = this.currentField.currentIds;\n        await this.props.record.discard();\n        this.action.doAction({\n            name: this.props.treeLabel,\n            type: \"ir.actions.act_window\",\n            res_model: this.currentField.resModel,\n            views: [\n                [false, \"list\"],\n                [false, \"form\"],\n            ],\n            domain: [[\"id\", \"in\", ids]],\n            context: {\n                form_view_ref: \"account.view_duplicated_moves_tree_js\",\n            },\n        });\n    }\n\n    async openFormAndDiscard(id) {\n        const action = await this.orm.call(this.currentField.resModel, \"action_open_business_doc\", [id], {});\n        await this.props.record.discard();\n        this.action.doAction(action);\n    }\n\n    get currentField() {\n        return this.props.record.data[this.props.name];\n    }\n}\n\nX2ManyButtons.template = \"account.X2ManyButtons\";\nregistry.category(\"fields\").add(\"x2many_buttons\", {\n    component: X2ManyButtons,\n    relatedFields: [{ name: \"display_name\", type: \"char\" }],\n    extractProps: ({ string }) => ({ treeLabel: string || _t(\"Records\") }),\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { escape } from \"@web/core/utils/strings\";\nimport { markup } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\n\nexport class AccountMoveService {\n    constructor(env, services) {\n        this.setup(env, services);\n    }\n\n    setup(env, services) {\n        this.env = env;\n        this.action = services.action;\n        this.dialog = services.dialog;\n        this.orm = services.orm;\n    }\n\n    async addDeletionDialog(component, moveIds) {\n        const isMoveEndOfChain = await this.orm.call(\"account.move\", \"check_move_sequence_chain\", [moveIds]);\n        if (!isMoveEndOfChain) {\n            const message = _t(\"This operation will create a gap in the sequence.\");\n            const confirmationDialogProps = component.deleteConfirmationDialogProps;\n            confirmationDialogProps.body = markup(`<div class=\"text-danger\">${escape(message)}</div>${escape(confirmationDialogProps.body)}`);\n            this.dialog.add(ConfirmationDialog, confirmationDialogProps);\n            return true;\n        }\n        return false;\n    }\n\n    async downloadPdf(accountMoveId) {\n        const downloadAction = await this.orm.call(\n            \"account.move\",\n            \"action_invoice_download_pdf\",\n            [accountMoveId]\n        );\n        await this.action.doAction(downloadAction);\n    }\n}\n\nexport const accountMoveService = {\n    dependencies: [\"action\", \"dialog\", \"orm\"],\n    start(env, services) {\n        return new AccountMoveService(env, services);\n    },\n};\n\nregistry.category(\"services\").add(\"account_move\", accountMoveService);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\n\nexport const accountNotificationService = {\n    dependencies: [\"bus_service\", \"notification\", \"action\"],\n\n    start(env, { bus_service, notification, action }) {\n        bus_service.subscribe(\"account_notification\", ({ message, sticky, title, type, action_button}) => {\n            const buttons = [{\n                name: action_button.name,\n                primary: false,\n                onClick: () => {\n                    action.doAction({\n                        name: _t(action_button.action_name),\n                        type: 'ir.actions.act_window',\n                        res_model: action_button.model,\n                        domain: [[\"id\", \"in\", action_button.res_ids]],\n                        views: [[false, 'list'], [false, 'form']],\n                        target: 'current',\n                    });\n                },\n            }];\n            notification.add(message, { sticky, title, type, buttons });\n        });\n    }\n};\n\nregistry.category(\"services\").add(\"accountNotification\", accountNotificationService);\n", "import { user } from \"@web/core/user\";\nimport { AccountFileUploader } from \"@account/components/account_file_uploader/account_file_uploader\";\nimport { UploadDropZone } from \"@account/components/upload_drop_zone/upload_drop_zone\";\nimport { KanbanDropdownMenuWrapper } from \"@web/views/kanban/kanban_dropdown_menu_wrapper\";\nimport { KanbanRecord } from \"@web/views/kanban/kanban_record\";\n\nimport { useState, onWillStart } from \"@odoo/owl\";\n\n// Accounting Dashboard\nexport class DashboardKanbanDropdownMenuWrapper extends KanbanDropdownMenuWrapper {\n    onClick(ev) {\n        // Keep the dropdown open as we need the fileupload to remain in the dom\n        if (!ev.target.tagName === \"INPUT\" && !ev.target.closest('.file_upload_kanban_action_a')) {\n            super.onClick(ev);\n        }\n    }\n}\n\nexport class DashboardKanbanRecord extends KanbanRecord {\n    static template = \"account.DashboardKanbanRecord\";\n    static components = {\n        ...DashboardKanbanRecord.components,\n        UploadDropZone,\n        AccountFileUploader,\n        KanbanDropdownMenuWrapper: DashboardKanbanDropdownMenuWrapper,\n    };\n\n    setup() {\n        super.setup();\n        onWillStart(async () => {\n            this.allowDrop = this.recordDropSettings.group ? await user.hasGroup(this.recordDropSettings.group) : true;\n        });\n        this.dropzoneState = useState({\n            visible: false,\n        });\n    }\n\n    get recordDropSettings() {\n        return JSON.parse(this.props.record.data.kanban_dashboard).drag_drop_settings;\n    }\n\n    get dropzoneProps() {\n        const {image, text} = this.recordDropSettings;\n        return {\n            visible: this.dropzoneState.visible,\n            dragIcon: image,\n            dragText: text,\n            dragTitle: this.props.record.data.name,\n            hideZone: () => { this.dropzoneState.visible = false; },\n        }\n    }\n}\n", "import { KanbanRenderer } from \"@web/views/kanban/kanban_renderer\";\nimport { DashboardKanbanRecord } from \"./account_dashboard_kanban_record\";\n\nimport { useSubEnv, reactive } from \"@odoo/owl\";\n\nexport class DashboardKanbanRenderer extends KanbanRenderer {\n    static template = \"account.DashboardKanbanRenderer\";\n    static components = {\n        ...KanbanRenderer.components,\n        KanbanRecord: DashboardKanbanRecord,\n    };\n\n    setup() {\n        super.setup();\n        useSubEnv({\n            dashboardState: reactive({isDragging: false}),\n            setDragging: this.setDragging.bind(this),\n        });\n    }\n\n    kanbanDragEnter(e) {\n        this.setDragging(e.dataTransfer.types.includes(\"Files\"));\n    }\n\n    kanbanDragLeave(e) {\n        const mouseX = e.clientX, mouseY = e.clientY;\n        const {x, y, width, height} = this.rootRef.el.getBoundingClientRect();\n        const mouseInsideKanbanRenderer = mouseX > x && mouseX <= x + width && mouseY > y && mouseY <= y + height;\n        if (!mouseInsideKanbanRenderer || !e.dataTransfer.types.includes(\"Files\")) {\n            // if the mouse position is outside the kanban renderer, all cards should hide their dropzones.\n            this.setDragging(false);\n        } else {\n            this.setDragging(true);\n        }\n    }\n\n    kanbanDragDrop(e) {\n        this.setDragging(false);\n        return false;\n    }\n\n    setDragging(value) {\n        this.env.dashboardState.isDragging = value;\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { DashboardKanbanRenderer } from \"./account_dashboard_kanban_renderer\";\n\nexport const accountDashboardKanbanView = {\n    ...kanbanView,\n    Renderer: DashboardKanbanRenderer,\n};\n\nregistry.category(\"views\").add(\"account_dashboard_kanban\", accountDashboardKanbanView);\n", "import { FileUploadKanbanController } from \"../file_upload_kanban/file_upload_kanban_controller\";\nimport { AccountFileUploader } from \"@account/components/account_file_uploader/account_file_uploader\";\n\nexport class AccountMoveKanbanController extends FileUploadKanbanController {\n    static components = {\n        ...FileUploadKanbanController.components,\n        AccountFileUploader,\n    };\n\n    setup() {\n        super.setup();\n        this.showUploadButton = this.props.context.default_move_type !== 'entry' || 'active_id' in this.props.context;\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { fileUploadKanbanView } from \"../file_upload_kanban/file_upload_kanban_view\";\nimport { AccountMoveKanbanController } from \"./account_move_kanban_controller\";\n\nexport const accountMoveUploadKanbanView = {\n    ...fileUploadKanbanView,\n    Controller: AccountMoveKanbanController,\n    buttonTemplate: \"account.AccountMoveKanbanView.Buttons\",\n};\n\nregistry.category(\"views\").add(\"account_documents_kanban\", accountMoveUploadKanbanView);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { FileUploadListController } from \"../file_upload_list/file_upload_list_controller\";\nimport { AccountFileUploader } from \"@account/components/account_file_uploader/account_file_uploader\";\n\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class AccountMoveListController extends FileUploadListController {\n    static components = {\n        ...FileUploadListController.components,\n        AccountFileUploader,\n    };\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.account_move_service = useService(\"account_move\");\n        this.showUploadButton = this.props.context.default_move_type !== 'entry' || 'active_id' in this.props.context;\n    }\n\n    get actionMenuProps() {\n        const actionMenuProps = {\n            ...super.actionMenuProps,\n            printDropdownTitle: _t(\"Download\"),\n        };\n        if (this.props.resModel === \"account.move\") {\n            actionMenuProps.loadExtraPrintItems = this.loadExtraPrintItems.bind(this);\n        }\n        return actionMenuProps;\n    }\n\n    async loadExtraPrintItems() {\n        return this.orm.call(\"account.move\", \"get_extra_print_items\", [this.actionMenuProps.getActiveIds()]);\n    }\n\n    async onDeleteSelectedRecords() {\n        const selectedResIds = await this.getSelectedResIds();\n        if (this.props.resModel !== \"account.move\" || !await this.account_move_service.addDeletionDialog(this, selectedResIds)) {\n            return super.onDeleteSelectedRecords(...arguments);\n        }\n    }\n}\n", "import { BillGuide } from \"@account/components/bill_guide/bill_guide\";\nimport { FileUploadListRenderer } from \"../file_upload_list/file_upload_list_renderer\";\n\nexport class AccountMoveListRenderer extends FileUploadListRenderer {\n    static template = \"account.AccountMoveListRenderer\";\n    static components = {\n        ...FileUploadListRenderer.components,\n        BillGuide,\n    };\n\n    // Add warning background color in the ref column if we detect that the move has a duplicated\n    getCellClass(column, record) {\n        const classNames = super.getCellClass(column, record);\n        if (column.name === 'ref' && record.data.duplicated_ref_ids && record.data.duplicated_ref_ids.count !== 0) {\n            return `${classNames} table-warning`;\n        }\n        return classNames;\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { fileUploadListView } from \"../file_upload_list/file_upload_list_view\";\nimport { AccountMoveListController } from \"./account_move_list_controller\";\nimport { AccountMoveListRenderer } from \"./account_move_list_renderer\";\n\nexport const accountMoveUploadListView = {\n    ...fileUploadListView,\n    Controller: AccountMoveListController,\n    Renderer: AccountMoveListRenderer,\n    buttonTemplate: \"account.AccountMoveListView.Buttons\",\n};\n\nregistry.category(\"views\").add(\"account_tree\", accountMoveUploadListView);\n", "import { ListController } from \"@web/views/list/list_controller\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport {registry} from \"@web/core/registry\";\nimport {listView} from \"@web/views/list/list_view\";\n\nexport class AccountX2ManyListController extends ListController {\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n    }\n\n    async openRecord(record) {\n        const action = await this.orm.call(record.resModel, 'action_open_business_doc', [record.resId], {});\n        return this.actionService.doAction(action);\n    }\n}\n\nregistry.category(\"views\").add(\"account_x2many_list\", {\n    ...listView,\n    Controller: AccountX2ManyListController,\n});\n", "import { KanbanController } from \"@web/views/kanban/kanban_controller\";\nimport { DocumentFileUploader } from \"@account/components/document_file_uploader/document_file_uploader\";\n\nexport class FileUploadKanbanController extends KanbanController {\n    static components = {\n        ...KanbanController.components,\n        DocumentFileUploader,\n    };\n}\n", "import { KanbanRenderer } from \"@web/views/kanban/kanban_renderer\";\nimport { UploadDropZone } from \"@account/components/upload_drop_zone/upload_drop_zone\";\nimport { useState } from \"@odoo/owl\";\nimport { uploadFileFromData } from \"../upload_file_from_data_hook\";\n\nexport class FileUploadKanbanRenderer extends KanbanRenderer {\n    static template = \"account.FileUploadKanbanRenderer\";\n    static components = {\n        ...KanbanRenderer.components,\n        UploadDropZone,\n    };\n\n    setup() {\n        super.setup();\n        this.dropzoneState = useState({ visible: false });\n        this.uploadFileFromData = uploadFileFromData();\n    }\n\n    async onPaste(ev) {\n        if (!ev.clipboardData?.items) {\n            return;\n        }\n        ev.preventDefault();\n        this.uploadFileFromData(ev.clipboardData);\n    }\n\n    onDragStart(ev) {\n        if (ev.dataTransfer.types.includes(\"Files\")) {\n            this.dropzoneState.visible = true;\n        }\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { FileUploadKanbanController } from \"./file_upload_kanban_controller\";\nimport { FileUploadKanbanRenderer } from \"./file_upload_kanban_renderer\";\n\nexport const fileUploadKanbanView = {\n    ...kanbanView,\n    Controller: FileUploadKanbanController,\n    Renderer: FileUploadKanbanRenderer,\n    buttonTemplate: \"account.FileuploadKanbanView.Buttons\",\n};\n\nregistry.category(\"views\").add(\"file_upload_kanban\", fileUploadKanbanView);\n", "import { ListController } from \"@web/views/list/list_controller\";\nimport { DocumentFileUploader } from \"@account/components/document_file_uploader/document_file_uploader\";\n\nexport class FileUploadListController extends ListController {\n    static components = {\n        ...ListController.components,\n        DocumentFileUploader,\n    };\n};\n", "import { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { UploadDropZone } from \"@account/components/upload_drop_zone/upload_drop_zone\";\nimport { useState } from \"@odoo/owl\";\nimport { uploadFileFromData } from \"../upload_file_from_data_hook\";\n\nexport class FileUploadListRenderer extends ListRenderer {\n    static template = \"account.FileUploadListRenderer\";\n    static components = {\n        ...ListRenderer.components,\n        UploadDropZone,\n    };\n\n    setup() {\n        super.setup();\n        this.dropzoneState = useState({ visible: false });\n        this.uploadFileFromData = uploadFileFromData();\n    }\n\n    async onPaste(ev) {\n        if (!ev.clipboardData?.items) {\n            return;\n        }\n        ev.preventDefault();\n        this.uploadFileFromData(ev.clipboardData);\n    }\n\n    onDragStart(ev) {\n        if (ev.dataTransfer.types.includes(\"Files\")) {\n            this.dropzoneState.visible = true;\n        }\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { listView } from \"@web/views/list/list_view\";\nimport { FileUploadListController } from \"./file_upload_list_controller\";\nimport { FileUploadListRenderer } from \"./file_upload_list_renderer\";\n\nexport const fileUploadListView = {\n    ...listView,\n    Controller: FileUploadListController,\n    Renderer: FileUploadListRenderer,\n    buttonTemplate: \"account.FileuploadListView.Buttons\",\n};\n\nregistry.category(\"views\").add(\"file_upload_list\", fileUploadListView);\n", "// Supported file types we need extract on paste\nconst supportedFileTypes = [\"text/xml\", \"application/pdf\"];\n\n/**\n * Return function to extract and upload from given dataTransfer.\n *\n * @param {dataTransfer} dataTransfer containing text or files.\n */\nexport function uploadFileFromData(dataTransfer) {\n    return async (dataTransfer) => {\n\n        function uploadFiles(dataTransfer) {\n            const invalidFiles = [...dataTransfer.items].filter(\n                (item) => item.kind !== \"file\" || !supportedFileTypes.includes(item.type)\n            );\n            if (invalidFiles.length !== 0) {\n                // don't upload any files if one of them is non supported file type\n                console.warn(\"Invalid files to extract details.\");\n                return;\n            }\n            let uploadInput = document.querySelector('.document_file_uploader.o_input_file');\n            uploadInput.files = dataTransfer.files;\n            uploadInput.dispatchEvent(new Event(\"change\"));\n        }\n\n        if (dataTransfer.files.length !== 0) {\n            uploadFiles(dataTransfer);\n        } else {\n            console.warn(\"Invalid data to extract details.\");\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\nexport const accountTourSteps = {\n    goToAccountMenu(description=\"Open Invoicing Menu\") {\n        return stepUtils.goToAppSteps('account.menu_finance', description);\n    },\n    onboarding() {\n        return [];\n    },\n    newInvoice() {\n        return [\n            {\n                trigger: \"button.o_list_button_add\",\n                content: _t(\"Now, we'll create your first invoice\"),\n                run: \"click\",\n            },\n        ];\n    },\n}\n\nregistry.category(\"web_tour.tours\").add('account_tour', {\n    url: \"/odoo\",\n    steps: () => [\n    ...accountTourSteps.goToAccountMenu(markup(_t('Send invoices to your customers in no time with the <b>Invoicing app</b>.'))),\n    ...accountTourSteps.onboarding(),\n    ...accountTourSteps.newInvoice(),\n    {\n        trigger: \"div[name=partner_id] .o_input_dropdown\",\n        content: markup(_t(\"Write a customer name to <b>create one</b> or <b>see suggestions</b>.\")),\n        tooltipPosition: \"right\",\n        run: \"click\",\n    },\n    {\n        isActive: [\"auto\"],\n        trigger: \"div[name=partner_id] input\",\n        run: \"edit Test Customer\",\n    },\n    {\n        isActive: [\"auto\"],\n        trigger: \".o_m2o_dropdown_option a:contains('Create')\",\n        content: _t(\"Select first partner\"),\n        run: \"click\",\n    },\n    {\n        isActive: [\"auto\"],\n        trigger: \".modal-content button.btn-primary\",\n        content: markup(_t(\"Once everything is set, you are good to continue. You will be able to edit this later in the <b>Customers</b> menu.\")),\n        run: \"click\",\n    },\n    {\n        trigger: \"div[name=invoice_line_ids] .o_field_x2many_list_row_add a\",\n        content: _t(\"Add a line to your invoice\"),\n        run: \"click\",\n    },\n    {\n        trigger: \"div[name=invoice_line_ids] div[name=product_id]\",\n        content: _t(\"Fill in the details of the product or see the suggestion.\"),\n        tooltipPosition: \"bottom\",\n        run: \"click\",\n    },\n    {\n        isActive: [\"auto\"],\n        trigger: \"div[name=invoice_line_ids] div[name=product_id] input\",\n        run: \"edit Test Product\",\n    },\n    {\n        isActive: [\"auto\"],\n        trigger: \"div[name=invoice_line_ids] div[name=product_id] .o_m2o_dropdown_option_create a:contains(create)\",\n        content: _t(\"Create the product.\"),\n        run: \"click\",\n    },\n    {\n        trigger: \"div[name=invoice_line_ids] div[name=product_id] button[id=labelVisibilityButtonId]\",\n        content: _t(\"Click here to add a description to your product.\"),\n        tooltipPosition: \"bottom\",\n        run: \"click\",\n    },\n    {\n        trigger: \"div[name=invoice_line_ids] div[name=product_id] textarea\",\n        content: _t(\"Add a description to your item.\"),\n        tooltipPosition: \"bottom\",\n        run: \"edit A very useful description.\",\n    },\n    {\n        isActive: [\"auto\"],\n        trigger: \"div[name=invoice_line_ids] div[name=product_id] textarea\",\n        run: function () {\n            // Since the t-on-change of the input is not triggered by the run: \"edit\" action,\n            // we need to dispatch the event manually requiring a function.\n            const input = this.anchor;\n            input.dispatchEvent(new InputEvent(\"input\"));\n            input.dispatchEvent(new Event(\"change\"));\n        },\n    },\n    {\n        trigger: \"div[name=invoice_line_ids] td[name=price_unit]\",\n        content: _t(\"Verify the price and update if necessary.\"),\n        tooltipPosition: \"bottom\",\n        run: \"click\",\n    },\n    {\n        isActive: [\"auto\"],\n        trigger: \"div[name=invoice_line_ids] div[name=price_unit] input\",\n        content: _t(\"Set a price.\"),\n        run: \"edit 100\",\n    },\n    ...stepUtils.saveForm(),\n    {\n        trigger: \"button[name=action_post]\",\n        content: _t(\"Once your invoice is ready, confirm it.\"),\n        run: \"click\",\n    },\n    {\n        trigger: \"button[name=action_invoice_sent]:contains(send)\",\n        content: _t(\"Send the invoice to the customer and check what he'll receive.\"),\n        tooltipPosition: \"bottom\",\n        run: \"click\",\n    },\n    {\n        trigger: \".o_field_widget[name=mail_partner_ids] input\",\n        content: _t(\"Send the invoice to the customer and check what he'll receive.\"),\n        tooltipPosition: \"bottom\",\n        run: \"edit Test Customer\",\n    },\n    {\n        isActive: [\"auto\"],\n        trigger: \".ui-menu-item a:contains('Test Customer')\",\n        content: _t(\"Select first partner\"),\n        run: \"click\",\n    },\n    {\n        isActive: [\"auto\"],\n        trigger: \".o_field_widget[name=email] input, input[name=email]\",\n        content: markup(_t(\"Write here <b>your own email address</b> to test the flow.\")),\n        run: \"edit customer@example.com\",\n    },\n    {\n        isActive: [\"auto\"],\n        trigger: \".modal button.o_form_button_save\",\n        content: _t(\"Validate.\"),\n        run: \"click\",\n    },\n    {\n        trigger: \".modal button[name=action_send_and_print]\",\n        content: _t(\"Let's send the invoice.\"),\n        tooltipPosition: \"top\",\n        run: \"click\",\n    },\n    {\n        trigger: \"button[name=action_invoice_sent]:contains(send).btn-secondary\",\n        content: _t(\"The invoice having been sent, the button has changed priority.\"),\n        run() {},\n    },\n]});\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { SearchBar } from \"@web/search/search_bar/search_bar\";\n\npatch(SearchBar.prototype, {\n    getPreposition(searchItem) {\n        let preposition = super.getPreposition(searchItem);\n        if (\n            this.fields[searchItem.fieldName].name === 'payment_date'\n            || this.fields[searchItem.fieldName].name === 'next_payment_date'\n        ) {\n            preposition = _t(\"until\");\n        }\n        return preposition\n    }\n});\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 = null } = {}) {\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 = null } = {}) {\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.is_base_affected) {\n                            add_extra_base(other_tax, -1)\n                        }\n                    }\n                } else {\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            // Case: special_mode = 'total_excluded'\n            } else {\n                if (tax.include_base_amount) {\n                    for (const other_tax of get_tax_after()) {\n                        if (other_tax.is_base_affected) {\n                            add_extra_base(other_tax, 1);\n                        }\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                        if (other_tax.is_base_affected) {\n                            add_extra_base(other_tax, 1);\n                        }\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    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 = null,\n            manual_tax_amounts = null,\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            let tax_amount = null;\n            if (manual_tax_amounts && tax.id in manual_tax_amounts) {\n                tax_amount = manual_tax_amounts[tax.id].tax_amount_currency;\n            } else {\n                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            }\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        let raw_base = quantity * price_unit;\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        };\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            const tax_data = taxes_data[tax.id];\n            if (!(\"tax_amount\" in tax_data)) {\n                continue;\n            }\n\n            // Base amount.\n            let base = null;\n            if (manual_tax_amounts && \"base_amount_currency\" in manual_tax_amounts[tax.id]) {\n                base = manual_tax_amounts[tax.id].base_amount_currency;\n            } else {\n                let 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                total_tax_amount += Object.values(taxes_data[tax.id].batch)\n                    .filter(other_tax => other_tax.has_negative_factor)\n                    .reduce((sum, other_tax) => sum + reverse_charge_taxes_data[other_tax.id].tax_amount, 0);\n                base = raw_base + taxes_data[tax.id].extra_base_for_base;\n                if (\n                    tax_data.price_include &&\n                    (!special_mode || special_mode === \"total_included\")\n                ) {\n                    base -= total_tax_amount;\n                }\n            }\n            tax_data.base = base;\n\n            // Reverse charge.\n            if (tax.has_negative_factor) {\n                const reverse_charge_tax_data = reverse_charge_taxes_data[tax.id];\n                reverse_charge_tax_data.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        const currency = (\n            load('currency_id', null)\n            || load('company_currency_id', null)\n            || load('company_id', {}).currency_id\n            || {}\n        )\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: currency,\n            sign: load('sign', 1.0),\n            special_mode: kwargs.special_mode || null,\n            special_type: kwargs.special_type || null,\n            rate: load(\"rate\", 1.0),\n            manual_tax_amounts: kwargs.manual_tax_amounts || null,\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                manual_tax_amounts: base_line.manual_tax_amounts\n            }\n        );\n\n        const rate = base_line.rate;\n        const tax_details = base_line.tax_details = {\n            raw_total_excluded_currency: taxes_computation.total_excluded,\n            raw_total_excluded: rate ? taxes_computation.total_excluded / rate : 0.0,\n            raw_total_included_currency: taxes_computation.total_included,\n            raw_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.raw_total_excluded = roundPrecision(tax_details.raw_total_excluded, currency_pd);\n            tax_details.raw_total_included = roundPrecision(tax_details.raw_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                raw_tax_amount_currency: tax_data.tax_amount,\n                raw_tax_amount: tax_amount,\n                raw_base_amount_currency: tax_data.base_amount,\n                raw_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 total_per_tax = {};\n        const total_per_base = {};\n\n        for (const base_line of base_lines) {\n            const currency = base_line.currency_id;\n            const tax_details = base_line.tax_details;\n            tax_details.total_excluded_currency = roundPrecision(\n                tax_details.raw_total_excluded_currency,\n                currency.rounding\n            );\n            tax_details.total_excluded = roundPrecision(\n                tax_details.raw_total_excluded,\n                company.currency_id.rounding\n            );\n            tax_details.delta_total_excluded_currency = 0.0;\n            tax_details.delta_total_excluded = 0.0;\n            tax_details.total_included_currency = roundPrecision(\n                tax_details.raw_total_included_currency,\n                currency.rounding\n            );\n            tax_details.total_included = roundPrecision(\n                tax_details.raw_total_included,\n                company.currency_id.rounding\n            );\n            const taxes_data = tax_details.taxes_data;\n\n            // If there are taxes on it, account the amounts from taxes_data.\n            let index = 0;\n            for (const tax_data of taxes_data) {\n                const tax = tax_data.tax;\n                tax_data.tax_amount_currency = roundPrecision(\n                    tax_data.raw_tax_amount_currency,\n                    currency.rounding\n                );\n                tax_data.tax_amount = roundPrecision(\n                    tax_data.raw_tax_amount,\n                    company.currency_id.rounding\n                );\n                tax_data.base_amount_currency = roundPrecision(\n                    tax_data.raw_base_amount_currency,\n                    currency.rounding\n                );\n                tax_data.base_amount = roundPrecision(\n                    tax_data.raw_base_amount,\n                    company.currency_id.rounding\n                );\n\n                const tax_rounding_key = [tax.id, currency.id, base_line.is_refund, tax_data.is_reverse_charge];\n                if (!(tax_rounding_key in total_per_tax)) {\n                    total_per_tax[tax_rounding_key] = {\n                        tax: tax,\n                        is_reverse_charge: tax_data.is_reverse_charge,\n                        currency: currency,\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 tax_amounts = total_per_tax[tax_rounding_key];\n                tax_amounts.tax_amount_currency += tax_data.tax_amount_currency;\n                tax_amounts.raw_tax_amount_currency += tax_data.raw_tax_amount_currency;\n                tax_amounts.tax_amount += tax_data.tax_amount;\n                tax_amounts.raw_tax_amount += tax_data.raw_tax_amount;\n                tax_amounts.base_amount_currency += tax_data.base_amount_currency;\n                tax_amounts.raw_base_amount_currency += tax_data.raw_base_amount_currency;\n                tax_amounts.base_amount += tax_data.base_amount;\n                tax_amounts.raw_base_amount += tax_data.raw_base_amount;\n                if (!base_line.special_type) {\n                    tax_amounts.base_lines.push(base_line);\n                }\n\n                if (index === 0) {\n                    const base_rounding_key = [currency.id, base_line.is_refund];\n                    if (!(base_rounding_key in total_per_base)) {\n                        total_per_base[base_rounding_key] = {\n                            currency: currency,\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                            base_lines: [],\n                        };\n                    }\n\n                    const base_amounts = total_per_base[base_rounding_key];\n                    base_amounts.base_amount_currency += tax_data.base_amount_currency;\n                    base_amounts.raw_base_amount_currency += tax_data.raw_base_amount_currency;\n                    base_amounts.base_amount += tax_data.base_amount;\n                    base_amounts.raw_base_amount += tax_data.raw_base_amount;\n                    if (!base_line.special_type) {\n                        base_amounts.base_lines.push(base_line);\n                    }\n                }\n\n                index++;\n            }\n\n            // If not, just account the base amounts.\n            if(!taxes_data.length){\n                const tax_rounding_key = [null, currency.id, base_line.is_refund, false];\n                if (!(tax_rounding_key in total_per_tax)) {\n                    total_per_tax[tax_rounding_key] = {\n                        tax: null,\n                        currency: currency,\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                const tax_amounts = total_per_tax[tax_rounding_key];\n                tax_amounts.base_amount_currency += tax_details.total_excluded_currency;\n                tax_amounts.raw_base_amount_currency += tax_details.raw_total_excluded_currency;\n                tax_amounts.base_amount += tax_details.total_excluded;\n                tax_amounts.raw_base_amount += tax_details.raw_total_excluded;\n                if(!base_line.special_type){\n                    tax_amounts.base_lines.push(base_line);\n                }\n\n                const base_rounding_key = [currency.id, base_line.is_refund];\n                if (!(base_rounding_key in total_per_base)) {\n                    total_per_base[base_rounding_key] = {\n                        currency: currency,\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                        base_lines: []\n                    };\n                }\n                const base_amounts = total_per_base[base_rounding_key];\n                base_amounts.base_amount_currency += tax_details.total_excluded_currency;\n                base_amounts.raw_base_amount_currency += tax_details.raw_total_excluded_currency;\n                base_amounts.base_amount += tax_details.total_excluded;\n                base_amounts.raw_base_amount += tax_details.raw_total_excluded;\n                if(!base_line.special_type){\n                    base_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(\n                amounts.raw_tax_amount_currency,\n                amounts.currency.rounding\n            );\n            amounts.raw_tax_amount = roundPrecision(\n                amounts.raw_tax_amount,\n                company.currency_id.rounding\n            );\n            amounts.raw_base_amount_currency = roundPrecision(\n                amounts.raw_base_amount_currency,\n                amounts.currency.rounding\n            );\n            amounts.raw_base_amount = roundPrecision(\n                amounts.raw_base_amount,\n                company.currency_id.rounding\n            );\n        }\n\n        // Round 'total_per_base'.\n        for (const amounts of Object.values(total_per_base)) {\n            amounts.raw_base_amount_currency = roundPrecision(\n                amounts.raw_base_amount_currency,\n                amounts.currency.rounding\n            );\n            amounts.raw_base_amount = roundPrecision(\n                amounts.raw_base_amount,\n                company.currency_id.rounding\n            );\n        }\n\n        // Dispatch the delta in term of tax amounts across the tax details when dealing with the 'round_globally' method.\n        // Suppose 2 lines:\n        // - quantity=12.12, price_unit=12.12, tax=23%\n        // - quantity=12.12, price_unit=12.12, tax=23%\n        // The tax of each line is computed as round(12.12 * 12.12 * 0.23) = 33.79\n        // The expected tax amount of the whole document is round(12.12 * 12.12 * 0.23 * 2) = 67.57\n        // The delta in term of tax amount is 67.57 - 33.79 - 33.79 = -0.01\n        for (const tax_amounts of Object.values(total_per_tax)) {\n            if (!tax_amounts.base_lines.length) {\n                continue;\n            }\n\n            const base_line = tax_amounts.base_lines.sort(\n                (a, b) =>\n                    a.tax_details.total_included_currency - b.tax_details.total_included_currency\n            )[0];\n            tax_amounts.reference_base_line = base_line;\n            const tax = tax_amounts.tax;\n            if(!tax){\n                continue;\n            }\n\n            const tax_details = base_line.tax_details;\n            const delta_tax_amount_currency = tax_amounts.raw_tax_amount_currency - tax_amounts.tax_amount_currency;\n            const delta_tax_amount = tax_amounts.raw_tax_amount - tax_amounts.tax_amount;\n\n            const tax_data = tax_details.taxes_data.find(x => x.tax.id === tax_amounts.tax.id && x.is_reverse_charge === tax_amounts.is_reverse_charge);\n            tax_amounts.reference_tax_data = tax_data;\n            tax_data.tax_amount_currency += delta_tax_amount_currency;\n            tax_data.tax_amount += delta_tax_amount;\n        }\n\n        // Dispatch the delta of base amounts accross the base lines.\n        // Suppose 2 lines:\n        // - quantity=12.12, price_unit=12.12, tax=23%\n        // - quantity=12.12, price_unit=12.12, tax=23%\n        // The base amount of each line is computed as round(12.12 * 12.12) = 146.89\n        // The expected base amount of the whole document is round(12.12 * 12.12 * 2) = 293.79\n        // The delta in term of base amount is 293.79 - 146.89 - 146.89 = 0.01\n        for (const tax_amounts of Object.values(total_per_tax)) {\n            const base_line = tax_amounts.reference_base_line;\n            if (!base_line){\n                continue;\n            }\n\n            const delta_base_amount_currency = tax_amounts.raw_base_amount_currency - tax_amounts.base_amount_currency;\n            const delta_base_amount = tax_amounts.raw_base_amount - tax_amounts.base_amount;\n            if (floatIsZero(delta_base_amount_currency, tax_amounts.currency.decimal_places) && floatIsZero(delta_base_amount, company.currency_id.decimal_places)) {\n                continue;\n            }\n\n            const tax_details = base_line.tax_details;\n            const tax_data = tax_amounts.reference_tax_data;\n            if (tax_data) {\n                tax_data.base_amount_currency += delta_base_amount_currency;\n                tax_data.base_amount += delta_base_amount;\n            } else {\n                tax_details.delta_total_excluded_currency += delta_base_amount_currency;\n                tax_details.delta_total_excluded += delta_base_amount;\n\n                const base_rounding_key = [tax_amounts.currency.id, base_line.is_refund];\n                const base_amounts = total_per_base[base_rounding_key];\n                base_amounts.base_amount_currency += delta_base_amount_currency;\n                base_amounts.base_amount += delta_base_amount;\n            }\n        }\n\n        // Dispatch the delta of base amounts accross the base lines.\n        // Suppose 2 lines:\n        // - quantity=12.12, price_unit=12.12, tax=23%\n        // - quantity=12.12, price_unit=12.12, tax=13%\n        // The base amount of each line is computed as round(12.12 * 12.12) = 146.89\n        // The expected base amount of the whole document is round(12.12 * 12.12 * 2) = 293.79\n        // Currently, the base amount has already been rounded per tax. So the tax details for the whole document is currently:\n        // 23%: base = 146.89, tax = 33.79\n        // 13%: base = 146.89, tax = 19.1\n        // However, for the whole document, there is a delta in term of base amount: 293.79 - 146.89 - 146.89 = 0.01\n        // This delta won't be there in any base but still has to be accounted.\n        for (const base_amounts of Object.values(total_per_base)) {\n            if (!base_amounts.base_lines.length) {\n                continue;\n            }\n\n            const base_line = base_amounts.base_lines.sort(\n                (a, b) =>\n                    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 delta_base_amount_currency = base_amounts.raw_base_amount_currency - base_amounts.base_amount_currency;\n            const delta_base_amount = base_amounts.raw_base_amount - base_amounts.base_amount;\n            if (floatIsZero(delta_base_amount_currency, base_amounts.currency.decimal_places) && floatIsZero(delta_base_amount, company.currency_id.decimal_places)) {\n                continue;\n            }\n\n            tax_details.delta_total_excluded_currency += delta_base_amount_currency;\n            tax_details.delta_total_excluded += delta_base_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            tax_totals_summary.base_amount_currency += values.total_excluded_currency;\n            tax_totals_summary.base_amount += values.total_excluded;\n            tax_totals_summary.tax_amount_currency += values.tax_amount_currency;\n            tax_totals_summary.tax_amount += values.tax_amount;\n        }\n\n        // Tax groups.\n        const untaxed_amount_subtotal_label = _t(\"Untaxed Amount\");\n        const subtotals = {};\n\n        const tax_group_grouping_function = (base_line, tax_data) => {\n            return {\n                grouping_key: tax_data.tax.tax_group_id.id,\n                raw_grouping_key: tax_data.tax.tax_group_id,\n            };\n        }\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)) {\n                subtotals[preceding_subtotal] = {\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            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        // Subtotals.\n        if (!Object.keys(subtotals).length) {\n            subtotals[untaxed_amount_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\n        const ordered_subtotals = Array.from(Object.entries(subtotals))\n            .sort((a, b) => (subtotals_order[a[0]] || 0) - (subtotals_order[b[0]] || 0));\n        let accumulated_tax_amount_currency = 0.0;\n        let accumulated_tax_amount = 0.0;\n        for (const [subtotal_label, subtotal] of ordered_subtotals) {\n            subtotal.name = subtotal_label;\n            subtotal.base_amount_currency = tax_totals_summary.base_amount_currency + accumulated_tax_amount_currency;\n            subtotal.base_amount = tax_totals_summary.base_amount + accumulated_tax_amount;\n            for (const tax_group of subtotal.tax_groups) {\n                subtotal.tax_amount_currency += tax_group.tax_amount_currency;\n                subtotal.tax_amount += tax_group.tax_amount;\n                accumulated_tax_amount_currency += tax_group.tax_amount_currency;\n                accumulated_tax_amount += tax_group.tax_amount;\n            }\n            tax_totals_summary.subtotals.push(subtotal);\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 cash_rounding_method = cash_rounding.rounding_method;\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, cash_rounding_method);\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] = tax_totals_summary.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        // Subtract the cash rounding from the untaxed amounts.\n        const cash_rounding_base_amount_currency = tax_totals_summary.cash_rounding_base_amount_currency || 0.0;\n        const cash_rounding_base_amount = tax_totals_summary.cash_rounding_base_amount || 0.0;\n        tax_totals_summary.base_amount_currency -= cash_rounding_base_amount_currency;\n        tax_totals_summary.base_amount -= cash_rounding_base_amount;\n        for (const subtotal of tax_totals_summary.subtotals) {\n            subtotal.base_amount_currency -= cash_rounding_base_amount_currency;\n            subtotal.base_amount -= cash_rounding_base_amount;\n        }\n        encountered_base_amounts.add(parseFloat(tax_totals_summary.base_amount_currency.toFixed(currency.decimal_places)));\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 + cash_rounding_base_amount_currency;\n        tax_totals_summary.total_amount = tax_totals_summary.base_amount + tax_totals_summary.tax_amount + cash_rounding_base_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            const generated_grouping_key = grouping_function(base_line, tax_data);\n            let raw_grouping_key = generated_grouping_key;\n            let grouping_key = generated_grouping_key;\n\n            // There is no FrozenDict in javascript.\n            // When the key is a record, it can't be jsonified so this is a trick to provide both the\n            // raw_grouping_key (to be jsonified) from the grouping_key (to be added to the values).\n            if (typeof raw_grouping_key === 'object' && (\"raw_grouping_key\" in raw_grouping_key)) {\n                raw_grouping_key = generated_grouping_key.raw_grouping_key;\n                grouping_key = generated_grouping_key.grouping_key;\n            }\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                    total_excluded_currency: tax_details.total_excluded_currency + tax_details.delta_total_excluded_currency,\n                    total_excluded: tax_details.total_excluded + tax_details.delta_total_excluded,\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 + tax_details.delta_total_excluded_currency,\n                base_amount: tax_details.total_excluded + tax_details.delta_total_excluded,\n                raw_base_amount_currency: tax_details.raw_total_excluded_currency,\n                raw_base_amount: tax_details.raw_total_excluded,\n                total_excluded_currency: tax_details.total_excluded_currency + tax_details.delta_total_excluded_currency,\n                total_excluded: tax_details.total_excluded + tax_details.delta_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            'total_excluded_currency',\n            'total_excluded'\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", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport {\n    copyClipboardButtonField,\n    CopyClipboardButtonField,\n} from \"@web/views/fields/copy_clipboard/copy_clipboard_field\";\n\nimport { CopyButton } from \"@web/core/copy_button/copy_button\";\n\nclass PaymentWizardCopyButton extends CopyButton {\n    async onClick() {\n        await this.env.model.mutex.getUnlockedDef();\n        return super.onClick();\n    }\n}\n\nclass PaymentWizardCopyClipboardButtonField extends CopyClipboardButtonField {\n    static components = { CopyButton: PaymentWizardCopyButton };\n}\n\nconst paymentWizardCopyClipboardButtonField = {\n    ...copyClipboardButtonField,\n    component: PaymentWizardCopyClipboardButtonField,\n};\n\nregistry\n    .category(\"fields\")\n    .add(\"PaymentWizardCopyClipboardButtonField\", paymentWizardCopyClipboardButtonField);\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nconst exampleData = {\n    ghostColumns: [_t('Ideas'), _t('Design'), _t('Review'), _t('Send'), _t('Done')],\n    applyExamplesText: _t(\"Use This For My Campaigns\"),\n    allowedGroupBys: ['stage_id'],\n    examples: [{\n        name: _t('Creative Flow'),\n        columns: [_t('Ideas'), _t('Design'), _t('Review'), _t('Send'), _t('Done')],\n        description: _t(\"Collect ideas, design creative content and publish it once reviewed.\"),\n    }, {\n        name: _t('Event-driven Flow'),\n        columns: [_t('Later'), _t('This Month'), _t('This Week'), _t('Running'), _t('Sent')],\n        description: _t(\"Track incoming events (e.g. Christmas, Black Friday, ...) and publish timely content.\"),\n    }, {\n        name: _t('Soft-Launch Flow'),\n        columns: [_t('Pre-Launch'), _t('Soft-Launch'), _t('Deploy'), _t('Report'), _t('Done')],\n        description: _t(\"Prepare your Campaign, test it with part of your audience and deploy it fully afterwards.\"),\n    }, {\n        name: _t('Audience-driven Flow'),\n        columns: [_t('Gather Data'), _t('List-Building'), _t('Copywriting'), _t('Sent')],\n        description: _t(\"Gather data, build a recipient list and write content based on your Marketing target.\"),\n    }, {\n        name: _t('Approval-based Flow'),\n        columns: [_t('To be Approved'), _t('Approved'), _t('Deployed')],\n        description: _t(\"Prepare Campaigns and get them approved before making them go live.\"),\n    }],\n};\n\nregistry.category(\"kanban_examples\").add(\"utm_campaign\", exampleData);\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 { Component } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { SaleActionHelperDialog } from \"./sale_action_helper_dialog\"\n\nexport class SaleActionHelper extends Component {\n    static template = \"sale.SaleActionHelper\";\n    static props = {\n        noContentHelp: String,\n    }\n\n    setup() {\n        this.dialogService = useService(\"dialog\");\n    }\n\n    openVideoPreview() {\n        this.dialogService.add(SaleActionHelperDialog, {\n            url: \"https://www.youtube.com/embed/N4zw-2t6spk?autoplay=1\",\n        })\n    }\n};\n", "/** @odoo-module **/\n\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { Component } from \"@odoo/owl\";\n\nexport class SaleActionHelperDialog extends Component {\n    static components = { Dialog };\n    static template = \"sale.SaleActionHelperDialog\";\n    static props = {\n        url: String,\n        close: Function,\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        options: {\n            type: Object,\n            optional: true,\n            shape: {\n                showQuantity : { type: Boolean, optional: true },\n            },\n        },\n        save: Function,\n        discard: Function,\n        close: Function,\n    };\n    static defaultProps = {\n        options: {\n            showQuantity: true,\n        },\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        this._initSelectedComboItems();\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 (comboItem.is_configurable) {\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, // Hide the optional products, if any.\n                options: { canChangeVariant: false, showQuantity: false, showPrice: 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        if (quantity <= 0) quantity = 1;\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 for all units, formatted using the provided currency.\n     *\n     * @return {String} The formatted total price.\n     */\n    get formattedTotalPrice() {\n        return formatCurrency(this.state.quantity * this._comboPrice, 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     * Return the total price per unit.\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 {Number} The total price.\n     */\n    get _comboPrice() {\n        const extraPrice = Array.from(this.state.selectedComboItems.values()).reduce(\n            (price, item) => price + item.totalExtraPrice, 0\n        );\n        return 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, in the same order as the combos given as props.\n     *\n     * @return {ProductComboItem[]} The sorted selected combo items.\n     */\n    get _selectedComboItems() {\n        const sortedItems = new Map([...this.state.selectedComboItems.entries()].sort(\n            (entry1, entry2) =>\n                this.props.combos.findIndex(combo => combo.id === entry1[0])\n                - this.props.combos.findIndex(combo => combo.id === entry2[0])\n        ));\n        return Array.from(sortedItems.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 {boolean} is_configurable\n     * @param {ProductProduct|object} product\n     */\n    constructor({id, extra_price, is_selected, is_configurable, product}) {\n        this.id = id;\n        this.extra_price = extra_price;\n        this.is_selected = is_selected;\n        this.is_configurable = is_configurable;\n        this.product = new ProductProduct(product);\n    }\n\n    /**\n     * Return the combo item's \"total\" extra price.\n     *\n     * The total extra price is the sum of:\n     * - The combo item's extra price,\n     * - The extra price of the selected `no_variant` PTAVs of the combo item's product.\n     *\n     * @return {Number} The combo item's \"total\" extra price.\n     */\n    get totalExtraPrice() {\n        return this.extra_price + this.product.selectedNoVariantPtavsPriceExtra;\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     * @param {string} image_src\n     */\n    setup({id, product_tmpl_id, display_name, ptals, image_src}) {\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        this.image_src = image_src;\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     * 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';\nimport { getSelectedCustomPtav } from \"../sale_utils\";\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_256`;\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\n            || ptal.create_variant === 'no_variant'\n            || !!getSelectedCustomPtav(ptal);\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 (\n            ptal.selected_ptavs.length > 0 &&\n            (ptal.hasSelectedCustomPtav || ptal.create_variant === 'no_variant')\n        );\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                showQuantity : { type: Boolean, optional: true },\n                showPrice : { 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            showQuantity: this.props.options?.showQuantity ?? true,\n            showPrice: this.props.options?.showPrice ?? 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                    const ptav = ptavList.find(ptav => ptav.id === excludedPtavId);\n                    if (ptav) {\n                        ptav.excluded = true; // Assign only if the element exists\n                    }\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", "import { _t } from \"@web/core/l10n/translation\";\nimport { Component } from \"@odoo/owl\";\nimport { formatCurrency } from \"@web/core/currency\";\nimport { BadgeExtraPrice } from \"../badge_extra_price/badge_extra_price\";\nimport { getSelectedCustomPtav } from \"../sale_utils\";\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 !!getSelectedCustomPtav(this.props);\n    }\n\n    get showValuesChoice() {\n        return (this.env.canChangeVariant || this.props.create_variant === 'no_variant') && (\n            this.props.attribute_values.length > 1 || this.props.attribute.display_type === 'multi'\n        )\n    }\n\n    get customValuePlaceholder() {\n        return _t(\"Enter a customized value\");\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        btnClasses: { type: String, 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", "/** @odoo-module **/\n\nimport { registry } from '@web/core/registry';\nimport { CharField } from '@web/views/fields/char/char_field';\nimport {\n    productLabelSectionAndNoteOne2Many,\n    ProductLabelSectionAndNoteOne2Many,\n    ProductLabelSectionAndNoteListRender,\n} from '@account/components/product_label_section_and_note_field/product_label_section_and_note_field';\nimport {\n    listSectionAndNoteText,\n    sectionAndNoteFieldOne2Many,\n    sectionAndNoteText,\n    ListSectionAndNoteText,\n    SectionAndNoteText,\n} from '@account/components/section_and_note_fields_backend/section_and_note_fields_backend';\n\nexport class SaleOrderLineListRenderer extends ProductLabelSectionAndNoteListRender {\n    static recordRowTemplate = 'sale.ListRenderer.RecordRow';\n\n    /**\n     * Product description widget logic\n     */\n    getCellTitle(column, record) {\n        // When using this list renderer, we don't want the product_id cell to have a tooltip with\n        // its label.\n        if (column.name === 'product_id' || column.name === 'product_template_id') {\n            return;\n        }\n        super.getCellTitle(column, record);\n    }\n\n    getActiveColumns(list) {\n        let activeColumns = super.getActiveColumns(list);\n        let productTmplCol = activeColumns.find((col) => col.name === 'product_template_id');\n        let productCol = activeColumns.find((col) => col.name === 'product_id');\n\n        if (productCol && productTmplCol) {\n            // Hide the template column if the variant one is enabled.\n            activeColumns = activeColumns.filter((col) => col.name != 'product_template_id')\n        }\n\n        return activeColumns;\n    }\n\n    /**\n     * Combo logic\n     */\n\n    /**\n     * Whether the provided record is a section, a note, or a combo.\n     *\n     * This method's name isn't ideal since it doesn't mention combos, but we'd have to override a\n     * few other methods to fix this, and the added complexity isn't worth it.\n     *\n     * @param record The record to check\n     * @return {Boolean} Whether the record is a section, a note, or a combo.\n     */\n    isSectionOrNote(record=null) {\n        return super.isSectionOrNote(record) || this.isCombo(record);\n    }\n\n    getRowClass(record) {\n        let classNames = super.getRowClass(record);\n        if (this.isCombo(record) || this.isComboItem(record)) {\n            classNames = classNames.replace('o_row_draggable', '');\n        }\n        return `${classNames} ${this.isCombo(record) ? 'o_is_line_section' : ''}`;\n    }\n\n    isCellReadonly(column, record) {\n        return super.isCellReadonly(column, record) || (\n            this.isComboItem(record)\n                && ![this.titleField, 'tax_id', 'qty_delivered'].includes(column.name)\n        );\n    }\n\n    async onDeleteRecord(record) {\n        if (this.isCombo(record)) {\n            await record.update({ selected_combo_items: JSON.stringify([]) });\n        }\n        await super.onDeleteRecord(record);\n    }\n\n    isCombo(record) {\n        return record.data.product_type === 'combo';\n    }\n\n    isComboItem(record) {\n        return !!record.data.combo_item_id;\n    }\n}\n\nexport class SaleOrderLineOne2Many extends ProductLabelSectionAndNoteOne2Many {\n    static components = {\n        ...ProductLabelSectionAndNoteOne2Many.components,\n        ListRenderer: SaleOrderLineListRenderer,\n    };\n}\nexport const saleOrderLineOne2Many = {\n    ...productLabelSectionAndNoteOne2Many,\n    component: SaleOrderLineOne2Many,\n    additionalClasses: sectionAndNoteFieldOne2Many.additionalClasses,\n};\n\nregistry.category('fields').add('sol_o2m', saleOrderLineOne2Many);\n\nexport class SaleOrderLineText extends SectionAndNoteText {\n    get componentToUse() {\n        return this.props.record.data.product_type === 'combo' ? CharField : super.componentToUse;\n    }\n}\n\nexport class ListSaleOrderLineText extends ListSectionAndNoteText {\n    get componentToUse() {\n        return this.props.record.data.product_type === 'combo' ? CharField : super.componentToUse;\n    }\n}\n\nexport const saleOrderLineText = {\n    ...sectionAndNoteText,\n    component: SaleOrderLineText,\n};\n\nexport const listSaleOrderLineText = {\n    ...listSectionAndNoteText,\n    component: ListSaleOrderLineText,\n};\n\nregistry.category('fields').add('sol_text', saleOrderLineText);\nregistry.category('fields').add('list.sol_text', listSaleOrderLineText);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport {\n    KanbanProgressBarField,\n    kanbanProgressBarField,\n} from \"@web/views/fields/progress_bar/kanban_progress_bar_field\";\nimport { useEffect } from \"@odoo/owl\";\n\n/**\n * A custom Component for the view of sales teams on the kanban view in the CRM app.\n *\n * The wanted behavior is to show a progress bar when an invoicing target is defined or show\n * a link redirecting to the record's form view otherwise.\n */\nexport class SaleProgressBarField extends KanbanProgressBarField {\n    static template = \"sale.SaleProgressBarField\";\n    /**\n     * Anything used by the component is defined on the setup method.\n     */\n    setup() {\n        super.setup();\n\n        this.actionService = useService(\"action\");\n        this.orm = useService(\"orm\");\n\n        useEffect(() => {\n            this.state.isInvoicingTargetDefined = this.props.record.data[this.props.maxValueField];\n        });\n    }\n\n    /**\n     * Display the form view of the record on click.\n     */\n    async defineInvoicingTarget() {\n        const { resId, resModel } = this.props.record;\n        const action = await this.orm.call(resModel, \"get_formview_action\", [[resId]]);\n        this.actionService.doAction(action, { props: { mode: \"edit\" } });\n    }\n}\n\nexport const saleProgressBarField = {\n    ...kanbanProgressBarField,\n    component: SaleProgressBarField,\n};\n\nregistry.category(\"fields\").add(\"sales_team_progressbar\", saleProgressBarField);\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\";\nimport { markup } from \"@odoo/owl\";\n\nregistry.category(\"web_tour.tours\").add(\"sale_tour\", {\n    url: \"/odoo\",\n    steps: () => [\n        stepUtils.showAppsMenuItem(),\n        {\n            isActive: [\"community\"],\n            trigger: \".o_app[data-menu-xmlid='sale.sale_menu_root']\",\n            content: _t(\"Let\u2019s create a beautiful quotation in a few clicks .\"),\n            tooltipPosition: \"right\",\n            run: \"click\",\n        },\n        {\n            isActive: [\"enterprise\"],\n            trigger: \".o_app[data-menu-xmlid='sale.sale_menu_root']\",\n            content: _t(\"Let\u2019s create a beautiful quotation in a few clicks .\"),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_sale_order\",\n        },\n        {\n            trigger: \"button.o_list_button_add\",\n            content: _t(\"Build your first quotation right here!\"),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_sale_order\",\n        },\n        {\n            trigger: \".o_field_res_partner_many2one[name='partner_id'] input\",\n            content: _t(\"Search a customer name, or create one on the fly.\"),\n            tooltipPosition: \"right\",\n            run: \"edit Agrolait\",\n        },\n        {\n            isActive: [\"auto\"],\n            trigger: \".ui-menu-item > a:contains('Agrolait')\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_field_x2many_list_row_add > a\",\n            content: _t(\"Click here to add some products or services to your quotation.\"),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_sale_order\",\n        },\n        {\n            trigger: `\n                .o_field_widget[name='product_id'] input,\n                .o_field_widget[name='product_template_id'] input\n            `,\n            content: _t(\"Select a product, or create a new one on the fly.\"),\n            tooltipPosition: \"right\",\n            run: \"edit DESK0001\",\n        },\n        {\n            isActive: [\"auto\"],\n            trigger: \"a:contains('DESK0001')\",\n            run: \"click\",\n        },\n        {\n            trigger: \".oi-arrow-right\", // Wait for product creation\n        },\n        {\n            trigger: \".o_field_widget[name='price_unit'] input\",\n            content: _t(\"add the price of your product.\"),\n            tooltipPosition: \"right\",\n            run: \"edit 10.0 && click body\",\n        },\n        {\n            isActive: [\"auto\"],\n            trigger: \".o_field_cell[name='price_subtotal']:contains(10.00)\",\n            run: \"click\",\n        },\n        {\n            isActive: [\"auto\", \"mobile\"],\n            trigger: \".o_statusbar_buttons button[name='action_quotation_send']\",\n        },\n        ...stepUtils.statusbarButtonsSteps(\n            \"Send by Email\",\n            markup(_t(\"<b>Send the quote</b> to yourself and check what the customer will receive.\")),\n        ),\n        {\n            isActive: [\"body:not(:has(.modal-footer button.o_mail_send))\"],\n            trigger: \".modal-footer button[name='document_layout_save']\",\n            content: _t(\"let's continue\"),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \".modal-footer button.o_mail_send\",\n            content: _t(\"Go ahead and send the quotation.\"),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            isActive: [\"auto\"],\n            trigger: \"body:not(.modal-open)\",\n            run: \"click\",\n        },\n    ],\n});\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useEffect } from '@odoo/owl';\nimport { WarningDialog } from \"@web/core/errors/error_dialogs\";\nimport { serializeDateTime } from \"@web/core/l10n/dates\";\nimport { x2ManyCommands } from \"@web/core/orm_service\";\nimport { registry } from \"@web/core/registry\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport {\n    ProductLabelSectionAndNoteField,\n    productLabelSectionAndNoteField,\n} from \"@account/components/product_label_section_and_note_field/product_label_section_and_note_field\";\nimport { ProductConfiguratorDialog } from \"./product_configurator_dialog/product_configurator_dialog\";\nimport { uuid } from \"@web/views/utils\";\nimport { ComboConfiguratorDialog } from \"./combo_configurator_dialog/combo_configurator_dialog\";\nimport { ProductCombo } from \"./models/product_combo\";\nimport { getLinkedSaleOrderLines, serializeComboItem, getSelectedCustomPtav } from \"./sale_utils\";\n\nasync function applyProduct(record, product) {\n    // handle custom values & no variants\n    const customAttributesCommands = [\n        x2ManyCommands.set([]),  // Command.clear isn't supported in static_list/_applyCommands\n    ];\n    for (const ptal of product.attribute_lines) {\n        const selectedCustomPTAV = getSelectedCustomPtav(ptal);\n        if (selectedCustomPTAV) {\n            customAttributesCommands.push(\n                x2ManyCommands.create(undefined, {\n                    custom_product_template_attribute_value_id: [selectedCustomPTAV.id, \"we don't care\"],\n                    custom_value: ptal.customValue,\n                })\n            );\n        };\n    }\n\n    const noVariantPTAVIds = product.attribute_lines.filter(\n        ptal => ptal.create_variant === \"no_variant\"\n    ).flatMap(ptal => ptal.selected_attribute_value_ids);\n\n    // We use `_update` (not locked) instead of `update` (locked) so that multiple records can be\n    // updated in parallel (for performance).\n    await record._update({\n        product_id: [product.id, product.display_name],\n        product_uom_qty: product.quantity,\n        product_no_variant_attribute_value_ids: [x2ManyCommands.set(noVariantPTAVIds)],\n        product_custom_attribute_value_ids: customAttributesCommands,\n    });\n};\n\n\nexport class SaleOrderLineProductField extends ProductLabelSectionAndNoteField {\n    static template = \"sale.SaleProductField\";\n    static props = {\n        ...ProductLabelSectionAndNoteField.props,\n        readonlyField: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        super.setup();\n        this.dialog = useService(\"dialog\");\n        this.notification = useService(\"notification\");\n        this.orm = useService(\"orm\")\n        let isMounted = false;\n        let isInternalUpdate = false;\n        let wasCombo = false;\n        const { updateRecord } = this;\n        this.updateRecord = (value) => {\n            isInternalUpdate = true;\n            wasCombo = this.isCombo;\n            return updateRecord.call(this, value);\n        };\n        useEffect(value => {\n            if (!isMounted) {\n                isMounted = true;\n            } else if (value && isInternalUpdate) {\n                // we don't want to trigger product update when update comes from an external sources,\n                // such as an onchange, or the product configuration dialog itself\n                if (wasCombo) {\n                    // If the previously selected product was a combo, delete its selected combo\n                    // items before changing the product.\n                    this.props.record.update({ selected_combo_items: JSON.stringify([]) });\n                }\n                if (this.relation === \"product.template\" || this.isCombo) {\n                    this._onProductTemplateUpdate();\n                } else {\n                    this._onProductUpdate();\n                }\n            }\n            isInternalUpdate = false;\n        }, () => [Array.isArray(this.value) && this.value[0]]);\n    }\n\n    get productName() {\n        if (this.props.name == 'product_template_id') {\n            const product_id_data = this.props.record.data.product_id;\n            if (product_id_data && product_id_data[1]) {\n                return product_id_data[1].split(\"\\n\")[0];\n            }\n        }\n        return super.productName\n    }\n    get isProductClickable() {\n        // product form should be accessible if the widget field is readonly\n        // or if the line cannot be edited (e.g. locked SO)\n        return (\n            this.props.readonlyField ||\n            (this.props.record.model.root.activeFields.order_line &&\n                this.props.record.model.root._isReadonly(\"order_line\"))\n        );\n    }\n    get hasExternalButton() {\n        // Keep external button, even if field is specified as 'no_open' so that the user is not\n        // redirected to the product when clicking on the field content\n        const res = super.hasExternalButton;\n        return res || (!!this.props.record.data[this.props.name] && !this.state.isFloating);\n    }\n    get hasConfigurationButton() {\n        return this.isConfigurableLine || this.isConfigurableTemplate || this.isCombo;\n    }\n    get isConfigurableLine() {\n        return false;\n    }\n    get isConfigurableTemplate() {\n        return this.props.record.data.is_configurable_product;\n    }\n    get isCombo() {\n        return this.props.record.data.product_type === 'combo';\n    }\n    get isDownpayment() {\n        return this.props.record.data.is_downpayment;\n    }\n\n    get configurationButtonHelp() {\n        return _t(\"Edit Configuration\");\n    }\n\n    /**\n     * @override\n     */\n    get sectionAndNoteClasses() {\n        const className = super.sectionAndNoteClasses;\n        if (!className && !this.productName && !this.isDownpayment) {\n            return \"text-warning\";\n        }\n        return className;\n    }\n\n    onClick(ev) {\n        // Override to get internal link to products in SOL that cannot be edited\n        if (this.props.readonly) {\n            ev.stopPropagation();\n            this.openAction();\n        } else {\n            super.onClick(ev);\n        }\n    }\n\n    async _onProductTemplateUpdate() {\n        const result = await this.orm.call(\n            'product.template',\n            'get_single_product_variant',\n            [this.props.record.data.product_template_id[0]],\n            {\n                context: this.context,\n            }\n        );\n        if(result && result.product_id) {\n            if (this.props.record.data.product_id != result.product_id.id) {\n                if (result.is_combo) {\n                    await this.props.record.update({\n                        product_id: [result.product_id, result.product_name],\n                    });\n                    this._openComboConfigurator();\n                } else if (result.has_optional_products) {\n                    this._openProductConfigurator();\n                } else {\n                    await this.props.record.update({\n                        product_id: [result.product_id, result.product_name],\n                    });\n                    this._onProductUpdate();\n                }\n            }\n        } else {\n            if (result && result.sale_warning) {\n                const {type, title, message} = result.sale_warning\n                if (type === 'block') {\n                    // display warning block, and remove blocking product\n                    this.dialog.add(WarningDialog, { title, message });\n                    this.props.record.update({'product_template_id': false})\n                    return\n                } else if (type == 'warning') {\n                    // show the warning but proceed with the configurator opening\n                    this.notification.add(message, {\n                        title,\n                        type: \"warning\",\n                    });\n                }\n            }\n            if (!result.mode || result.mode === 'configurator') {\n                this._openProductConfigurator();\n            } else {\n                // only triggered when sale_product_matrix is installed.\n                this._openGridConfigurator();\n            }\n        }\n    }\n\n    async _onProductUpdate() {} // event_booth_sale, event_sale, sale_renting\n\n    onEditConfiguration() {\n        if (this.isConfigurableLine) {\n            this._editLineConfiguration();\n        } else if (this.isCombo) {\n            this._openComboConfigurator(true);\n        } else if (this.isConfigurableTemplate) {\n            this._openProductConfigurator(true);\n        }\n    }\n    _editLineConfiguration() {} // event_booth_sale, event_sale, sale_renting\n\n    async _openProductConfigurator(edit=false) {\n        const saleOrderRecord = this.props.record.model.root;\n        const saleOrderLine = this.props.record.data;\n        let ptavIds = this._getVariantPtavIds(saleOrderLine);\n        let customPtavs = [];\n\n        if (edit) {\n            /**\n             * no_variant and custom attribute don't need to be given to the configurator for new\n             * products.\n             */\n            ptavIds.push(...this._getNoVariantPtavIds(saleOrderLine));\n            customPtavs = await this._getCustomPtavs(saleOrderLine);\n        }\n\n        this.dialog.add(ProductConfiguratorDialog, {\n            productTemplateId: saleOrderLine.product_template_id[0],\n            ptavIds: ptavIds,\n            customPtavs: customPtavs,\n            quantity: saleOrderLine.product_uom_qty,\n            productUOMId: saleOrderLine.product_uom[0],\n            companyId: saleOrderRecord.data.company_id[0],\n            pricelistId: saleOrderRecord.data.pricelist_id[0],\n            currencyId: saleOrderLine.currency_id[0],\n            soDate: serializeDateTime(saleOrderRecord.data.date_order),\n            edit: edit,\n            save: async (mainProduct, optionalProducts) => {\n                await Promise.all([\n                    applyProduct(this.props.record, mainProduct),\n                    ...optionalProducts.map(async product => {\n                        const line = await saleOrderRecord.data.order_line.addNewRecord({\n                            position: 'bottom', mode: 'readonly'\n                        });\n                        await applyProduct(line, product);\n                    }),\n                ]);\n                this._onProductUpdate();\n                saleOrderRecord.data.order_line.leaveEditMode();\n            },\n            discard: () => {\n                saleOrderRecord.data.order_line.delete(this.props.record);\n            },\n            ...this._getAdditionalDialogProps(),\n        });\n    }\n\n    async _openComboConfigurator(edit=false) {\n        const saleOrder = this.props.record.model.root.data;\n        const comboLineRecord = this.props.record;\n        const comboItemLineRecords = getLinkedSaleOrderLines(comboLineRecord);\n        const selectedComboItems = await Promise.all(comboItemLineRecords.map(async record => ({\n            id: record.data.combo_item_id[0],\n            no_variant_ptav_ids: edit ? this._getNoVariantPtavIds(record.data) : [],\n            custom_ptavs: edit ? await this._getCustomPtavs(record.data) : [],\n        })));\n        const { combos, ...remainingData } = await rpc('/sale/combo_configurator/get_data', {\n            product_tmpl_id: comboLineRecord.data.product_template_id[0],\n            currency_id: comboLineRecord.data.currency_id[0],\n            quantity: comboLineRecord.data.product_uom_qty,\n            date: serializeDateTime(saleOrder.date_order),\n            company_id: saleOrder.company_id[0],\n            pricelist_id: saleOrder.pricelist_id[0],\n            selected_combo_items: selectedComboItems,\n            ...this._getAdditionalRpcParams(),\n        });\n        this.dialog.add(ComboConfiguratorDialog, {\n            combos: combos.map(combo => new ProductCombo(combo)),\n            ...remainingData,\n            company_id: saleOrder.company_id[0],\n            pricelist_id: saleOrder.pricelist_id[0],\n            date: serializeDateTime(saleOrder.date_order),\n            edit: edit,\n            save: async (comboProductData, selectedComboItems) => {\n                saleOrder.order_line.leaveEditMode();\n                const comboLineValues = {\n                    product_uom_qty: comboProductData.quantity,\n                    selected_combo_items: JSON.stringify(\n                        selectedComboItems.map(serializeComboItem)\n                    ),\n                };\n                if (!edit) {\n                    comboLineValues.virtual_id = uuid();\n                }\n                await comboLineRecord.update(comboLineValues);\n                // Ensure that the order lines are sorted according to their sequence.\n                await saleOrder.order_line._sort();\n            },\n            discard: () => saleOrder.order_line.delete(comboLineRecord),\n            ...this._getAdditionalDialogProps(),\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     * Hook to append additional props in overriding modules.\n     *\n     * @return {Object} The additional props.\n     */\n    _getAdditionalDialogProps() {\n        return {};\n    }\n\n    /**\n     * Return the PTAV ids of the provided sale order line.\n     *\n     * @param saleOrderLine The sale order line\n     * @return {Number[]} The sale order line's PTAV ids.\n     */\n    _getVariantPtavIds(saleOrderLine) {\n        return saleOrderLine.product_template_attribute_value_ids.records.map(\n            record => record.resId\n        );\n    }\n\n    /**\n     * Return the `no_variant` PTAV ids of the provided sale order line.\n     *\n     * @param saleOrderLine The sale order line\n     * @return {Number[]} The sale order line's `no_variant` PTAV ids.\n     */\n    _getNoVariantPtavIds(saleOrderLine) {\n        return saleOrderLine.product_no_variant_attribute_value_ids.records.map(\n            record => record.resId\n        );\n    }\n\n    /**\n     * Return the custom PTAVs of the provided sale order line.\n     *\n     * @param saleOrderLine The sale order line\n     * @return {Promise<CustomPtav[]>} The sale order line's custom PTAVs.\n     */\n    async _getCustomPtavs(saleOrderLine) {\n        // `product.attribute.custom.value` records are not loaded in the view because sub templates\n        // are not loaded in list views. Therefore, we fetch them from the server if the record was\n        // saved. Otherwise, we use the value stored on the line.\n        const customPtavIds = saleOrderLine.product_custom_attribute_value_ids;\n        const customPtavs = customPtavIds.records[0]?.isNew\n            ? customPtavIds.records.map(record => record.data)\n            : customPtavIds.currentIds.length\n                ? await this.orm.read(\n                    'product.attribute.custom.value',\n                    customPtavIds.currentIds,\n                    ['custom_product_template_attribute_value_id', 'custom_value'],\n                )\n                : [];\n        return customPtavs.map(customPtav => ({\n            id: customPtav.custom_product_template_attribute_value_id[0],\n            value: customPtav.custom_value,\n        }));\n    }\n}\n\nexport const saleOrderLineProductField = {\n    ...productLabelSectionAndNoteField,\n    component: SaleOrderLineProductField,\n    extractProps(fieldInfo, dynamicInfo) {\n        const props = productLabelSectionAndNoteField.extractProps(...arguments);\n        props.readonlyField = dynamicInfo.readonly;\n        return props;\n    },\n};\n\nregistry.category(\"fields\").add(\"sol_product_many2one\", saleOrderLineProductField);\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\n/**\n * Get the selected custom PTAV in the provided PTAL, if any.\n *\n * Note: a PTAL can have at most one selected custom PTAV, by design.\n *\n * @param {ProductTemplateAttributeLine.props} ptal The PTAL in which to look for the selected\n *     custom PTAV.\n * @return {Object|undefined} The selected custom PTAV, if any.\n *\n */\nexport function getSelectedCustomPtav(ptal) {\n    const selectedPtavIds = new Set(ptal.selected_attribute_value_ids);\n    return ptal.attribute_values.find(ptav => ptav.is_custom && selectedPtavIds.has(ptav.id));\n}\n", "import { FileUploadKanbanRenderer } from \"@account/views/file_upload_kanban/file_upload_kanban_renderer\";\nimport { SaleActionHelper } from \"../../js/sale_action_helper/sale_action_helper\";\n\nexport class SaleKanbanRenderer extends FileUploadKanbanRenderer {\n    static template = \"sale.SaleKanbanRenderer\";\n    static components = {\n        ...FileUploadKanbanRenderer.components,\n        SaleActionHelper,\n    };\n};\n\n", "import { SaleKanbanRenderer } from \"./sale_onboarding_kanban_renderer\";\nimport { fileUploadKanbanView } from \"@account/views/file_upload_kanban/file_upload_kanban_view\";\nimport { registry } from \"@web/core/registry\";\n\nexport const saleKanbanView = {\n    ...fileUploadKanbanView,\n    Renderer: SaleKanbanRenderer,\n};\n\nregistry.category(\"views\").add(\"sale_onboarding_kanban\", saleKanbanView);\n", "import { FileUploadListRenderer } from \"@account/views/file_upload_list/file_upload_list_renderer\";\nimport { SaleActionHelper } from \"../../js/sale_action_helper/sale_action_helper\";\n\nexport class SaleListRenderer extends FileUploadListRenderer {\n    static template = \"sale.SaleListRenderer\";\n    static components = {\n        ...FileUploadListRenderer.components,\n        SaleActionHelper,\n    };\n};\n", "import { registry } from \"@web/core/registry\";\nimport { fileUploadListView } from \"@account/views/file_upload_list/file_upload_list_view\";\nimport { SaleListRenderer } from \"./sale_onboarding_list_renderer\";\n\nexport const SaleListView = {\n    ...fileUploadListView,\n    Renderer: SaleListRenderer,\n};\n\nregistry.category(\"views\").add(\"sale_onboarding_list\", SaleListView);\n", "import { ActivityListPopoverItem } from \"@mail/core/web/activity_list_popover_item\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(ActivityListPopoverItem.prototype, {\n    get hasEditButton() {\n        return super.hasEditButton && !this.props.activity.calendar_event_id;\n    },\n\n    async onClickReschedule() {\n        await this.props.activity.rescheduleMeeting();\n    },\n});\n", "/** @odoo-module */\n\nimport { ActivityMenu } from \"@mail/core/web/activity_menu\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(ActivityMenu.prototype, {\n    openActivityGroup(group) {\n        if (group.model === \"calendar.event\") {\n            this.dropdown.close();\n            this.action.doAction(\"calendar.action_calendar_event\", {\n                additionalContext: {\n                    default_mode: \"day\",\n                    search_default_mymeetings: 1,\n                },\n                clearBreadcrumbs: true,\n            });\n        } else {\n            super.openActivityGroup(...arguments);\n        }\n    },\n});\n", "import { Activity } from \"@mail/core/web/activity_model\";\nimport { assignIn } from \"@mail/utils/common/misc\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Activity, {\n    _insert(data) {\n        const activity = super._insert(...arguments);\n        assignIn(activity, data, [\"calendar_event_id\"]);\n        return activity;\n    },\n});\n\npatch(Activity.prototype, {\n    async rescheduleMeeting() {\n        const action = await this.store.env.services.orm.call(\n            \"mail.activity\",\n            \"action_create_calendar_event\",\n            [[this.id]]\n        );\n        this.store.env.services.action.doAction(action);\n    },\n});\n", "import { Activity } from \"@mail/core/web/activity\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Activity.prototype, {\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n    },\n    async onClickReschedule() {\n        await this.props.activity.rescheduleMeeting();\n    },\n    /**\n     * @override\n     */\n    async unlink() {\n        if (this.props.activity.calendar_event_id) {\n            const thread = this.thread;\n            this.props.activity.remove();\n            await this.orm.call(\"mail.activity\", \"unlink_w_meeting\", [[this.props.activity.id]]);\n            this.props.onActivityChanged(thread);\n        } else {\n            super.unlink();\n        }\n    },\n});\n", "/** @odoo-module **/\n\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component } from \"@odoo/owl\";\n\nconst providerData = {\n    google: {\n        restart_sync_method: \"restart_google_synchronization\",\n        sync_route: \"/google_calendar/sync_data\",\n    },\n    microsoft: {\n        restart_sync_method: \"restart_microsoft_synchronization\",\n        sync_route: \"/microsoft_calendar/sync_data\",\n    },\n};\n\nexport class CalendarConnectProvider extends Component {\n    static props = {\n        ...standardWidgetProps,\n    };\n    static template = \"calendar.CalendarConnectProvider\";\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n    }\n\n    /**\n     * Activate the external sync for the first time, after installing the\n     * relevant submodule if necessary.\n     *\n     * @private\n     */\n    async onConnect(ev) {\n        ev.preventDefault();\n        ev.stopImmediatePropagation();\n        if (!(await this.props.record.save())) {\n            return; // handled by view\n        }\n        await this.orm.call(\n            this.props.record.resModel,\n            \"action_calendar_prepare_external_provider_sync\",\n            [this.props.record.resId]\n        );\n        // See google/microsoft_calendar for the origin of this shortened version\n        const { restart_sync_method, sync_route } =\n            providerData[this.props.record.data.external_calendar_provider];\n        await this.orm.call(\"res.users\", restart_sync_method, [[user.userId]]);\n        const response = await rpc(sync_route, {\n            model: \"calendar.event\",\n            fromurl: window.location.href,\n        });\n        await this._beforeLeaveContext();\n        if (response.status === \"need_auth\") {\n            window.location.assign(response.url);\n        } else if (response.status === \"need_refresh\") {\n            window.location.reload();\n        }\n    }\n    /**\n     * Hook to perform additional work before redirecting to external url or reloading.\n     *\n     * @private\n     */\n    async _beforeLeaveContext() {\n        return Promise.resolve();\n    }\n}\n\nconst calendarConnectProvider = {\n    component: CalendarConnectProvider,\n};\nregistry.category(\"view_widgets\").add(\"calendar_connect_provider\", calendarConnectProvider);\n", "/* @odoo-module */\n\nimport { Store } from \"@mail/core/common/store_service\";\n\nimport { deserializeDateTime, formatDateTime } from \"@web/core/l10n/dates\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {import(\"models\").Store} */\nconst StorePatch = {\n    onUpdateActivityGroups() {\n        super.onUpdateActivityGroups(...arguments);\n        for (const group of Object.values(this.activityGroups)) {\n            if (group.type === \"meeting\") {\n                for (const meeting of group.meetings) {\n                    if (meeting.start) {\n                        const date = deserializeDateTime(meeting.start);\n                        meeting.formattedStart = formatDateTime(date, {\n                            format: localization.timeFormat,\n                        });\n                    }\n                }\n            }\n        }\n    },\n};\npatch(Store.prototype, StorePatch);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { ConnectionLostError, rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\n\nexport const calendarNotificationService = {\n    dependencies: [\"action\", \"bus_service\", \"notification\"],\n\n    start(env, { action, bus_service, notification }) {\n        let calendarNotifTimeouts = {};\n        let nextCalendarNotifTimeout = null;\n        const displayedNotifications = new Set();\n\n        bus_service.subscribe(\"calendar.alarm\", (payload) => {\n            displayCalendarNotification(payload);\n        });\n        bus_service.start();\n\n        /**\n         * Displays the Calendar notification on user's screen\n         */\n        function displayCalendarNotification(notifications) {\n            let lastNotifTimer = 0;\n\n            // Clear previously set timeouts and destroy currently displayed calendar notifications\n            browser.clearTimeout(nextCalendarNotifTimeout);\n            Object.values(calendarNotifTimeouts).forEach((notif) => browser.clearTimeout(notif));\n            calendarNotifTimeouts = {};\n\n            // For each notification, set a timeout to display it\n            notifications.forEach(function (notif) {\n                const key = notif.event_id + \",\" + notif.alarm_id;\n                if (displayedNotifications.has(key)) {\n                    return;\n                }\n                calendarNotifTimeouts[key] = browser.setTimeout(function () {\n                    const notificationRemove = notification.add(notif.message, {\n                        title: notif.title,\n                        type: \"warning\",\n                        sticky: true,\n                        onClose: () => {\n                            displayedNotifications.delete(key);\n                        },\n                        buttons: [\n                            {\n                                name: _t(\"OK\"),\n                                primary: true,\n                                onClick: async () => {\n                                    await rpc(\"/calendar/notify_ack\");\n                                    notificationRemove();\n                                },\n                            },\n                            {\n                                name: _t(\"Details\"),\n                                onClick: async () => {\n                                    await action.doAction({\n                                        type: \"ir.actions.act_window\",\n                                        res_model: \"calendar.event\",\n                                        res_id: notif.event_id,\n                                        views: [[false, \"form\"]],\n                                    });\n                                    notificationRemove();\n                                },\n                            },\n                            {\n                                name: _t(\"Snooze\"),\n                                onClick: () => {\n                                    notificationRemove();\n                                },\n                            },\n                        ],\n                    });\n                    displayedNotifications.add(key);\n                }, notif.timer * 1000);\n                lastNotifTimer = Math.max(lastNotifTimer, notif.timer);\n            });\n\n            // Set a timeout to get the next notifications when the last one has been displayed\n            if (lastNotifTimer > 0) {\n                nextCalendarNotifTimeout = browser.setTimeout(\n                    getNextCalendarNotif,\n                    lastNotifTimer * 1000\n                );\n            }\n        }\n\n        async function getNextCalendarNotif() {\n            try {\n                const result = await rpc(\"/calendar/notify\", {}, { silent: true });\n                displayCalendarNotification(result);\n            } catch (error) {\n                if (!(error instanceof ConnectionLostError)) {\n                    throw error;\n                }\n            }\n        }\n    },\n};\n\nregistry.category(\"services\").add(\"calendarNotification\", calendarNotificationService);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { Component } from \"@odoo/owl\";\n\nexport class AskRecurrenceUpdatePolicyDialog extends Component {\n    static template = \"calendar.AskRecurrenceUpdatePolicyDialog\";\n    static components = {\n        Dialog,\n    };\n    static props = {\n        confirm: Function,\n        close: Function,\n    };\n\n    setup() {\n        this.possibleValues = {\n            self_only: {\n                checked: true,\n                label: _t(\"This event\"),\n            },\n            future_events: {\n                checked: false,\n                label: _t(\"This and following events\"),\n            },\n            all_events: {\n                checked: false,\n                label: _t(\"All events\"),\n            },\n        };\n    }\n\n    get selected() {\n        return Object.entries(this.possibleValues).find((state) => state[1].checked)[0];\n    }\n\n    set selected(val) {\n        this.possibleValues[this.selected].checked = false;\n        this.possibleValues[val].checked = true;\n    }\n\n    confirm() {\n        this.props.confirm(this.selected);\n        this.props.close();\n    }\n}\n", "/** @odoo-module **/\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { AskRecurrenceUpdatePolicyDialog } from \"@calendar/views/ask_recurrence_update_policy_dialog\";\n\nexport function askRecurrenceUpdatePolicy(dialogService) {\n    return new Promise((resolve) => {\n        dialogService.add(AskRecurrenceUpdatePolicyDialog, {\n            confirm: resolve,\n        }, {\n            onClose: resolve.bind(null, false),\n        });\n    });\n}\n\nexport function useAskRecurrenceUpdatePolicy() {\n    const dialogService = useService(\"dialog\");\n    return askRecurrenceUpdatePolicy.bind(null, dialogService);\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { CalendarController } from \"@web/views/calendar/calendar_controller\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { onWillStart } from \"@odoo/owl\";\nimport { CalendarQuickCreate } from \"@calendar/views/calendar_form/calendar_quick_create\";\nexport class AttendeeCalendarController extends CalendarController {\n    static template = \"calendar.AttendeeCalendarController\";\n    static components = {\n        ...AttendeeCalendarController.components,\n        QuickCreateFormView: CalendarQuickCreate,\n    };\n\n    setup() {\n        super.setup();\n        this.actionService = useService(\"action\");\n        this.orm = useService(\"orm\");\n        onWillStart(async () => {\n            this.isSystemUser = await user.hasGroup(\"base.group_system\");\n        });\n    }\n\n    onClickAddButton() {\n        this.actionService.doAction(\n            {\n                type: \"ir.actions.act_window\",\n                res_model: \"calendar.event\",\n                views: [[false, \"form\"]],\n            },\n            {\n                additionalContext: this.props.context,\n            }\n        );\n    }\n\n    goToFullEvent(resId, additionalContext) {\n        this.actionService.doAction(\n            {\n                type: \"ir.actions.act_window\",\n                res_model: \"calendar.event\",\n                views: [[false, \"form\"]],\n                res_id: resId || false,\n            },\n            {\n                additionalContext: {\n                    ...this.props.context,\n                    ...additionalContext,\n                },\n            }\n        );\n    }\n\n    getQuickCreateFormViewProps(record) {\n        const props = super.getQuickCreateFormViewProps(record);\n        const onDialogClosed = () => {\n            this.model.load();\n        };\n        return {\n            ...props,\n            size: \"md\",\n            goToFullEvent: (contextData) => {\n                const fullContext = {\n                    ...props.context,\n                    ...contextData,\n                };\n                this.goToFullEvent(false, fullContext);\n            },\n            onRecordSaved: () => onDialogClosed(),\n        };\n    }\n\n    async editRecord(record, context = {}) {\n        if (record.id) {\n            return this.goToFullEvent(record.id, context);\n        }\n    }\n\n    /**\n     * @override\n     *\n     * If the event is deleted by the organizer, the event is deleted, otherwise it is declined.\n     */\n    deleteRecord(record) {\n        if (\n            user.partnerId === record.attendeeId &&\n            user.partnerId === record.rawRecord.partner_id[0]\n        ) {\n            if (record.rawRecord.recurrency) {\n                this.openRecurringDeletionWizard(record);\n            } else {\n                super.deleteRecord(...arguments);\n            }\n        } else {\n            // Decline event\n            this.orm\n                .call(\"calendar.attendee\", \"do_decline\", [record.calendarAttendeeId])\n                .then(this.model.load.bind(this.model));\n        }\n    }\n\n    openRecurringDeletionWizard(record) {\n        this.actionService.doAction(\n            {\n                type: \"ir.actions.act_window\",\n                res_model: \"calendar.popover.delete.wizard\",\n                views: [[false, \"form\"]],\n                view_mode: \"form\",\n                name: \"Delete Recurring Event\",\n                context: { default_record: record.id },\n                target: \"new\",\n            },\n            {\n                onClose: () => {\n                    location.reload();\n                },\n            }\n        );\n    }\n\n    configureCalendarProviderSync(providerName) {\n        this.actionService.doAction({\n            name: _t(\"Connect your Calendar\"),\n            type: \"ir.actions.act_window\",\n            res_model: \"calendar.provider.config\",\n            views: [[false, \"form\"]],\n            view_mode: \"form\",\n            target: \"new\",\n            context: {\n                default_external_calendar_provider: providerName,\n                dialog_size: \"medium\",\n            },\n        });\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { user } from \"@web/core/user\";\nimport { CalendarModel } from \"@web/views/calendar/calendar_model\";\nimport { askRecurrenceUpdatePolicy } from \"@calendar/views/ask_recurrence_update_policy_hook\";\nimport { deleteConfirmationMessage, ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\n\nexport class AttendeeCalendarModel extends CalendarModel {\n    setup(params, { dialog }) {\n        super.setup(...arguments);\n        this.dialog = dialog;\n        this.rpc = rpc;\n    }\n\n    /**\n     * @override\n     */\n    async load() {\n        const res = await super.load(...arguments);\n        const [credentialStatus, syncStatus, defaultDuration] = await Promise.all([\n            rpc(\"/calendar/check_credentials\"),\n            this.orm.call(\"res.users\", \"check_synchronization_status\", [[user.userId]]),\n            this.orm.call(\"calendar.event\", \"get_default_duration\"),\n        ]);\n        this.syncStatus = syncStatus;\n        this.credentialStatus = credentialStatus;\n        this.defaultDuration = defaultDuration;\n        return res;\n    }\n\n    get attendees() {\n        return this.data.attendees;\n    }\n\n    /**\n     * @override\n     *\n     * Upon updating a record with recurrence, we need to ask how it will affect recurrent events.\n     */\n    async updateRecord(record) {\n        const rec = this.records[record.id];\n        if (rec.rawRecord.recurrency) {\n            const recurrenceUpdate = await askRecurrenceUpdatePolicy(this.dialog);\n            if (!recurrenceUpdate) {\n                return this.notify();\n            }\n            record.recurrenceUpdate = recurrenceUpdate;\n        }\n        return await super.updateRecord(...arguments);\n    }\n\n    /**\n     * @override\n     */\n    buildRawRecord(partialRecord, options = {}) {\n        const result = super.buildRawRecord(partialRecord, {\n            ...options,\n            duration_hour: this.defaultDuration,\n        });\n        if (partialRecord.recurrenceUpdate) {\n            result.recurrence_update = partialRecord.recurrenceUpdate;\n        }\n        return result;\n    }\n\n    /**\n     * Load the filter section and add both 'user' and 'everybody' filters to the context.\n     * @override\n     */\n    async loadFilterSection(fieldName, filterInfo, previousSection) {\n        let result = await super.loadFilterSection(fieldName, filterInfo, previousSection);\n        if (result?.filters) {\n            user.updateContext({\n                calendar_filters: {\n                    all: result?.filters?.find((f) => f.type == \"all\")?.active ?? false,\n                    user: result?.filters?.find((f) => f.type == \"user\")?.active ?? false,\n                }\n            });\n        }\n        return result;\n    }\n\n    /**\n     * @override\n     */\n    async updateData(data) {\n        await super.updateData(...arguments);\n        await this.updateAttendeeData(data);\n    }\n\n    /**\n     * Split the events to display an event for each attendee with the correct status.\n     * If the all filter is activated, we don't display an event for each attendee and keep\n     * the previous behavior to display a single event.\n     */\n    async updateAttendeeData(data) {\n        const attendeeFilters = data.filterSections.partner_ids;\n        let isEveryoneFilterActive = false;\n        let attendeeIds = [];\n        const eventIds = Object.keys(data.records).map((id) => Number.parseInt(id));\n        if (attendeeFilters) {\n            const allFilter = attendeeFilters.filters.find((filter) => filter.type === \"all\");\n            isEveryoneFilterActive = (allFilter && allFilter.active) || false;\n            attendeeIds = attendeeFilters.filters\n                .filter((filter) => filter.type !== \"all\" && filter.value)\n                .map((filter) => filter.value);\n        }\n        data.attendees = await this.orm.call(\"res.partner\", \"get_attendee_detail\", [\n            attendeeIds,\n            eventIds,\n        ]);\n        const currentPartnerId = user.partnerId;\n        if (!isEveryoneFilterActive) {\n            const activeAttendeeIds = new Set(\n                attendeeFilters.filters\n                    .filter((filter) => filter.type !== \"all\" && filter.value && filter.active)\n                    .map((filter) => filter.value)\n            );\n            // Duplicate records per attendee\n            const newRecords = {};\n            let duplicatedRecordIdx = -1;\n            for (const event of Object.values(data.records)) {\n                const eventData = event.rawRecord;\n                const attendees =\n                    eventData.partner_ids && eventData.partner_ids.length\n                        ? eventData.partner_ids\n                        : [eventData.partner_id[0]];\n                let duplicatedRecords = 0;\n                for (const attendee of attendees) {\n                    if (!activeAttendeeIds.has(attendee)) {\n                        continue;\n                    }\n                    // Records will share the same rawRecord.\n                    const record = { ...event };\n                    const attendeeInfo = data.attendees.find(\n                        (a) => a.id === attendee && a.event_id === event.id\n                    );\n                    record.attendeeId = attendee;\n                    // Colors are linked to the partner_id but in this case we want it linked\n                    // to attendeeId\n                    record.colorIndex = attendee;\n                    if (attendeeInfo) {\n                        record.attendeeStatus = attendeeInfo.status;\n                        record.isAlone = attendeeInfo.is_alone;\n                        record.isCurrentPartner = attendeeInfo.id === currentPartnerId;\n                        record.calendarAttendeeId = attendeeInfo.attendee_id;\n                    }\n                    const recordId = duplicatedRecords ? duplicatedRecordIdx-- : record.id;\n                    // Index in the records\n                    record._recordId = recordId;\n                    newRecords[recordId] = record;\n                    duplicatedRecords++;\n                }\n            }\n            data.records = newRecords;\n        } else {\n            for (const event of Object.values(data.records)) {\n                const eventData = event.rawRecord;\n                event.attendeeId = eventData.partner_id && eventData.partner_id[0];\n                const attendeeInfo = data.attendees.find(\n                    (a) => a.id === currentPartnerId && a.event_id === event.id\n                );\n                if (attendeeInfo) {\n                    event.isAlone = attendeeInfo.is_alone;\n                    event.calendarAttendeeId = attendeeInfo.attendee_id;\n                }\n            }\n        }\n    }\n\n    /**\n     * Archives a record, ask for the recurrence update policy in case of recurrent event.\n     */\n    async archiveRecord(record) {\n        let recurrenceUpdate = false;\n        if (record.rawRecord.recurrency) {\n            recurrenceUpdate = await askRecurrenceUpdatePolicy(this.dialog);\n            if (!recurrenceUpdate) {\n                return;\n            }\n        } else {\n            const confirm = await new Promise((resolve) => {\n                this.dialog.add(\n                    ConfirmationDialog,{\n                        title: _t(\"Bye-bye, record!\"),\n                        body: deleteConfirmationMessage,\n                        confirm: resolve.bind(null, true),\n                        confirmLabel: _t(\"Delete\"),\n                        cancel: () => resolve.bind(null, false),\n                        cancelLabel: _t(\"No, keep it\"),\n                    });\n            });\n            if (!confirm) {\n                return;\n            }\n        }\n        await this._archiveRecord(record.id, recurrenceUpdate);\n    }\n\n    async _archiveRecord(id, recurrenceUpdate) {\n        if (!recurrenceUpdate && recurrenceUpdate !== \"self_only\") {\n            await this.orm.call(this.resModel, \"action_archive\", [[id]]);\n        } else {\n            await this.orm.call(this.resModel, \"action_mass_archive\", [[id], recurrenceUpdate]);\n        }\n        await this.load();\n    }\n}\nAttendeeCalendarModel.services = [...CalendarModel.services, \"dialog\", \"orm\"];\n", "/** @odoo-module **/\n\nimport { CalendarRenderer } from \"@web/views/calendar/calendar_renderer\";\nimport { AttendeeCalendarCommonRenderer } from \"@calendar/views/attendee_calendar/common/attendee_calendar_common_renderer\";\nimport { AttendeeCalendarYearRenderer } from \"@calendar/views/attendee_calendar/year/attendee_calendar_year_renderer\";\n\nexport class AttendeeCalendarRenderer extends CalendarRenderer {\n    static components = {\n        ...CalendarRenderer.components,\n        day: AttendeeCalendarCommonRenderer,\n        week: AttendeeCalendarCommonRenderer,\n        month: AttendeeCalendarCommonRenderer,\n        year: AttendeeCalendarYearRenderer,\n    };\n}\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { calendarView } from \"@web/views/calendar/calendar_view\";\nimport { AttendeeCalendarController } from \"@calendar/views/attendee_calendar/attendee_calendar_controller\";\nimport { AttendeeCalendarModel } from \"@calendar/views/attendee_calendar/attendee_calendar_model\";\nimport { AttendeeCalendarRenderer } from \"@calendar/views/attendee_calendar/attendee_calendar_renderer\";\n\nexport const attendeeCalendarView = {\n    ...calendarView,\n    Controller: AttendeeCalendarController,\n    Model: AttendeeCalendarModel,\n    Renderer: AttendeeCalendarRenderer,\n};\n\nregistry.category(\"views\").add(\"attendee_calendar\", attendeeCalendarView);\n", "/** @odoo-module **/\n\nimport { onWillStart } from \"@odoo/owl\";\nimport { CalendarCommonPopover } from \"@web/views/calendar/calendar_common/calendar_common_popover\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useAskRecurrenceUpdatePolicy } from \"@calendar/views/ask_recurrence_update_policy_hook\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { user } from \"@web/core/user\";\n\nexport class AttendeeCalendarCommonPopover extends CalendarCommonPopover {\n    static components = {\n        ...CalendarCommonPopover.components,\n        Dropdown,\n        DropdownItem,\n    };\n    static subTemplates = {\n        ...CalendarCommonPopover.subTemplates,\n        body: \"calendar.AttendeeCalendarCommonPopover.body\",\n        footer: \"calendar.AttendeeCalendarCommonPopover.footer\",\n    };\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.actionService = useService(\"action\");\n        this.askRecurrenceUpdatePolicy = useAskRecurrenceUpdatePolicy();\n\n        onWillStart(this.onWillStart);\n    }\n\n    async onWillStart() {\n        // Show status dropdown if user is in attendees list\n        if (this.isEventEditable) {\n            const stateSelections = await this.env.services.orm.call(\n                this.props.model.resModel,\n                \"get_state_selections\"\n            );\n            this.statusColors = {\n                accepted: \"text-success\",\n                declined: \"text-danger\",\n                tentative: \"text-muted\",\n                needsAction: \"false\",\n            };\n            this.statusInfo = {};\n            for (const selection of stateSelections) {\n                this.statusInfo[selection[0]] = {\n                    text: selection[1],\n                    color: this.statusColors[selection[0]],\n                };\n            }\n            this.selectedStatusInfo = this.statusInfo[this.props.record.attendeeStatus];\n        }\n    }\n\n    get isCurrentUserAttendee() {\n        return (\n            this.props.record.rawRecord.partner_ids.includes(user.partnerId) ||\n            this.props.record.rawRecord.partner_id[0] === user.partnerId\n        );\n    }\n\n    get isCurrentUserOrganizer() {\n        return this.props.record.rawRecord.partner_id[0] === user.partnerId;\n    }\n\n    get isEventPrivate() {\n        return this.props.record.rawRecord.privacy === \"private\";\n    }\n\n    get displayAttendeeAnswerChoice() {\n        return (\n            this.props.record.rawRecord.partner_ids.some((partner) => partner !== user.partnerId) &&\n            this.props.record.isCurrentPartner\n        );\n    }\n\n    get isEventDetailsVisible() {\n        return this.isEventPrivate ? this.isEventEditable : true;\n    }\n\n    get isEventArchivable() {\n        return false;\n    }\n\n    async onClickOpenRecord() {\n        const action = await this.orm.call(\"calendar.event\", \"action_open_calendar_event\", [\n            this.props.record.id,\n        ]);\n        this.actionService.doAction(action);\n    }\n\n    /**\n     * @override\n     */\n    get isEventDeletable() {\n        return super.isEventDeletable && this.isEventEditable && !this.isEventArchivable;\n    }\n\n    /**\n     * @override\n     */\n    get isEventEditable() {\n        return this.props.record.rawRecord.user_can_edit;\n    }\n\n    get isEventViewable() {\n        return this.isEventPrivate ? this.isEventEditable : super.isEventEditable;\n    }\n\n    /**\n     * @override\n     */\n    get hasFooter() {\n        return this.isEventViewable || super.hasFooter;\n    }\n\n    async changeAttendeeStatus(selectedStatus) {\n        const record = this.props.record;\n        if (record.attendeeStatus === selectedStatus) {\n            return this.props.close();\n        }\n        let recurrenceUpdate = false;\n        if (record.rawRecord.recurrency) {\n            recurrenceUpdate = await this.askRecurrenceUpdatePolicy();\n            if (!recurrenceUpdate) {\n                return this.props.close();\n            }\n        }\n        await this.env.services.orm.call(this.props.model.resModel, \"change_attendee_status\", [\n            [record.id],\n            selectedStatus,\n            recurrenceUpdate,\n        ]);\n        await this.props.model.load();\n        this.props.close();\n    }\n\n    async onClickArchive() {\n        this.props.close();\n        await this.props.model.archiveRecord(this.props.record);\n    }\n}\n", "/** @odoo-module **/\n\nimport { CalendarCommonRenderer } from \"@web/views/calendar/calendar_common/calendar_common_renderer\";\nimport { AttendeeCalendarCommonPopover } from \"@calendar/views/attendee_calendar/common/attendee_calendar_common_popover\";\n\nexport class AttendeeCalendarCommonRenderer extends CalendarCommonRenderer {\n    static eventTemplate = \"calendar.AttendeeCalendarCommonRenderer.event\";\n    static components = {\n        ...CalendarCommonRenderer.components,\n        Popover: AttendeeCalendarCommonPopover,\n    };\n    /**\n     * @override\n     *\n     * Give a new key to our fc records to be able to iterate through in templates\n     */\n    convertRecordToEvent(record) {\n        let editable = false;\n        if (record && record.rawRecord) {\n            editable = record.rawRecord.user_can_edit;\n        }\n        return {\n            ...super.convertRecordToEvent(record),\n            id: record._recordId || record.id,\n            editable: editable,\n        };\n    }\n\n    /**\n     * @override\n     */\n    eventClassNames({ el, event }) {\n        const classesToAdd = super.eventClassNames(...arguments);\n        const record = this.props.model.records[event.id];\n        if (record) {\n            if (record.rawRecord.is_highlighted) {\n                classesToAdd.push(\"o_event_highlight\");\n            }\n            if (record.isAlone) {\n                classesToAdd.push(\"o_attendee_status_alone\");\n            } else {\n                classesToAdd.push(`o_attendee_status_${record.attendeeStatus}`);\n            }\n        }\n        return classesToAdd;\n    }\n\n    /**\n     * @override\n     */\n    onEventDidMount({ el, event }) {\n        super.onEventDidMount(...arguments);\n        const record = this.props.model.records[event.id];\n        if (record) {\n            if (this.env.searchModel?.context?.default_calendar_event_id === parseInt(event.id)) {\n                this.openPopover(el, record);\n            }\n        }\n    }\n\n    /**\n     * @override\n     *\n     * Allow slots to be selected over multiple days\n     */\n    isSelectionAllowed(event) {\n        return true;\n    }\n}\n", "/** @odoo-module **/\n\nimport { CalendarYearPopover } from \"@web/views/calendar/calendar_year/calendar_year_popover\";\n\nexport class AttendeeCalendarYearPopover extends CalendarYearPopover {\n    static subTemplates = {\n        ...CalendarYearPopover.subTemplates,\n        body: \"calendar.AttendeeCalendarYearPopover.body\",\n    };\n    getRecordClass(record) {\n        const classes = [super.getRecordClass(record)];\n        if (record.isAlone) {\n            classes.push(\"o_attendee_status_alone\");\n        } else {\n            classes.push(`o_attendee_status_${record.attendeeStatus}`);\n        }\n        return classes.join(\" \");\n    }\n}\n", "/** @odoo-module **/\n\nimport { CalendarYearRenderer } from \"@web/views/calendar/calendar_year/calendar_year_renderer\";\nimport { AttendeeCalendarYearPopover } from \"@calendar/views/attendee_calendar/year/attendee_calendar_year_popover\";\n\nexport class AttendeeCalendarYearRenderer extends CalendarYearRenderer {\n    static components = {\n        ...CalendarYearRenderer.components,\n        Popover: AttendeeCalendarYearPopover,\n    };\n}\n", "/** @odoo-module **/\n\nimport { FormController } from \"@web/views/form/form_controller\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { onWillStart } from \"@odoo/owl\";\n\nexport class CalendarFormController extends FormController {\n    setup() {\n        super.setup();\n        const ormService = useService(\"orm\");\n\n        onWillStart(async () => {\n            this.discussVideocallLocation = await ormService.call(\n                \"calendar.event\",\n                \"get_discuss_videocall_location\"\n            );\n        });\n    }\n\n    /**\n     * @override\n     */\n    async beforeExecuteActionButton(clickParams) {\n        const action = clickParams.name;\n        if (action === \"clear_videocall_location\" || action === \"set_discuss_videocall_location\") {\n            let newVal = \"\";\n            let videoCallSource = \"custom\";\n            let changes = {};\n            if (action === \"set_discuss_videocall_location\") {\n                newVal = this.discussVideocallLocation;\n                videoCallSource = \"discuss\";\n                changes.access_token = this.discussVideocallLocation.split(\"/\").pop();\n            }\n            changes = Object.assign(changes, {\n                videocall_location: newVal,\n                videocall_source: videoCallSource,\n            });\n            this.model.root.update(changes);\n            return false; // no continue\n        }\n        return super.beforeExecuteActionButton(...arguments);\n    }\n}\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { formView } from \"@web/views/form/form_view\";\nimport { CalendarFormController } from \"@calendar/views/calendar_form/calendar_form_controller\";\n\nexport const CalendarFormView = {\n    ...formView,\n    Controller: CalendarFormController,\n};\n\nregistry.category(\"views\").add(\"calendar_form\", CalendarFormView);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { FormViewDialog } from \"@web/views/view_dialogs/form_view_dialog\";\nimport { CalendarFormView } from \"./calendar_form_view\";\nimport { CalendarFormController } from \"./calendar_form_controller\";\nimport { serializeDate, serializeDateTime } from \"@web/core/l10n/dates\";\n\nconst QUICK_CREATE_CALENDAR_EVENT_FIELDS = {\n    name: { type: \"string\" },\n    start: { type: \"datetime\" },\n    start_date: { type: \"date\" },\n    stop_date: { type: \"date\" },\n    stop: { type: \"datetime\" },\n    allday: { type: \"boolean\" },\n    partner_ids: { type: \"many2many\" },\n    videocall_location: { type: \"string\" },\n    description: { type: \"string\" }\n};\n\nfunction getDefaultValuesFromRecord(data) {\n    const context = {};\n    for (let fieldName in QUICK_CREATE_CALENDAR_EVENT_FIELDS) {\n        if (fieldName in data) {\n            let value = data[fieldName];\n            const { type } = QUICK_CREATE_CALENDAR_EVENT_FIELDS[fieldName]\n            if (type === 'many2many') {\n                value = value.records.map((record) => record.resId);\n            } else if (type === 'date') {\n                value = value && serializeDate(value);\n            } else if (type === \"datetime\") {\n                value = value && serializeDateTime(value);\n            }\n            context[`default_${fieldName}`] = value || false;\n        }\n    }\n    return context;\n}\n\nexport class CalendarQuickCreateFormController extends CalendarFormController {\n    static props = {\n        ...CalendarFormController.props,\n        goToFullEvent: Function,\n    };\n\n    goToFullEvent() {\n        const context = getDefaultValuesFromRecord(this.model.root.data)\n        this.props.goToFullEvent(context);\n    }\n}\n\nregistry.category(\"views\").add(\"calendar_quick_create_form_view\", {\n    ...CalendarFormView,\n    Controller: CalendarQuickCreateFormController,\n});\n\nexport class CalendarQuickCreate extends FormViewDialog {\n    static props = {\n        ...FormViewDialog.props,\n        goToFullEvent: Function,\n    };\n\n    setup() {\n        super.setup();\n        Object.assign(this.viewProps, {\n            ...this.viewProps,\n            buttonTemplate: \"calendar.CalendarQuickCreateButtons\",\n            goToFullEvent: (contextData) => {\n                this.props.goToFullEvent(contextData);\n            }\n        });\n    }\n}\n", "/** @odoo-module **/\n\nimport { TagsList } from \"@web/core/tags_list/tags_list\";\n\nexport class AttendeeTagsList extends TagsList {\n    static template = \"calendar.AttendeeTagsList\";\n}\n", "import { registry } from \"@web/core/registry\";\nimport {\n    Many2ManyTagsAvatarField,\n    many2ManyTagsAvatarField,\n} from \"@web/views/fields/many2many_tags_avatar/many2many_tags_avatar_field\";\nimport { useSpecialData } from \"@web/views/fields/relational_utils\";\nimport { AttendeeTagsList } from \"@calendar/views/fields/attendee_tags_list\";\n\nconst ICON_BY_STATUS = {\n    accepted: \"fa-check\",\n    declined: \"fa-times\",\n    tentative: \"fa-question\",\n};\nexport class Many2ManyAttendee extends Many2ManyTagsAvatarField {\n    static template = \"calendar.Many2ManyAttendee\";\n    static components = {\n        ...Many2ManyAttendee.components,\n        TagsList: AttendeeTagsList,\n    };\n    setup() {\n        super.setup();\n        this.specialData = useSpecialData((orm, props) => {\n            const { context, name, record } = props;\n            return orm.call(\n                \"res.partner\",\n                \"get_attendee_detail\",\n                [record.data[name].records.map((rec) => rec.resId), [record.resId || false]],\n                {\n                    context,\n                }\n            );\n        });\n    }\n\n    get tags() {\n        const partnerIds = this.specialData.data;\n        const noEmailPartnerIds = this.props.record.data.invalid_email_partner_ids\n            ? this.props.record.data.invalid_email_partner_ids.records\n            : [];\n        const tags = super.tags.map((tag) => {\n            const partner = partnerIds.find((partner) => tag.resId === partner.id);\n            const noEmail = noEmailPartnerIds.find((partner) => (tag.resId == partner.resId));\n            if (partner) {\n                tag.status = partner.status;\n                tag.statusIcon = ICON_BY_STATUS[partner.status];\n            }\n            if (noEmail) {\n                tag.noEmail = true;\n            }\n            return tag;\n        });\n\n        const organizer = partnerIds.find((partner) => partner.is_organizer);\n        if (organizer) {\n            const orgId = organizer.id;\n            // sort elements according to the partner id\n            tags.sort((a, b) => {\n                const a_org = a.resId === orgId;\n                return a_org ? -1 : 1;\n            });\n        }\n        return tags;\n    }\n}\n\nexport const many2ManyAttendee = {\n    ...many2ManyTagsAvatarField,\n    component: Many2ManyAttendee,\n    additionalClasses: [\"o_field_many2many_tags_avatar\", \"w-100\"],\n};\n\nregistry.category(\"fields\").add(\"many2manyattendee\", many2ManyAttendee);\n", "/** @odoo-module **/\n\nimport { useState, useEffect } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { reposition } from \"@web/core/position/utils\";\nimport { Many2ManyAttendee, many2ManyAttendee } from \"@calendar/views/fields/many2many_attendee\";\n\nexport class Many2ManyAttendeeExpandable extends Many2ManyAttendee {\n    static template = \"calendar.Many2ManyAttendeeExpandable\";\n    state = useState({ expanded: false });\n\n    setup() {\n        super.setup();\n        this.attendeesCount = this.props.record.data.attendees_count;\n        this.acceptedCount = this.props.record.data.accepted_count;\n        this.declinedCount = this.props.record.data.declined_count;\n        this.uncertainCount = this.attendeesCount - this.acceptedCount - this.declinedCount;\n\n        if (!this.env.isSmall) {\n            useEffect(\n                () => {\n                    const popover = document\n                        .querySelector(\".o_field_many2manyattendeeexpandable\")\n                        .closest(\".o_popover\");\n                    const target = document.querySelector(\n                        `.fc-event[data-event-id=\"${this.props.record.resId}\"]`\n                    );\n                    reposition(popover, target, { position: \"right\", margin: 0 });\n                },\n                () => [this.state.expanded]\n            );\n        }\n    }\n\n    onExpanderClick() {\n        this.state.expanded = !this.state.expanded;\n    }\n}\n\nexport const many2ManyAttendeeExpandable = {\n    ...many2ManyAttendee,\n    component: Many2ManyAttendeeExpandable,\n};\n\nregistry.category(\"fields\").add(\"many2manyattendeeexpandable\", many2ManyAttendeeExpandable);\n", "import { listView } from \"@web/views/list/list_view\";\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\n\nexport class CalendarListModel extends listView.Model {\n    setup(params, { action, dialog, notification, rpc, user, view, company }) {\n        super.setup(...arguments);\n    }\n\n    /**\n    * @override\n    * Add the calendar view's selected attendees to the list view's domain.\n    */\n    async load(params = {}) {\n        const filters = params?.context?.calendar_filters;\n        const emptyDomain = Array.isArray(params?.domain) && params.domain.length == 0;\n        if (filters && emptyDomain) {\n            const selectedPartnerIds = await this.orm.call(\n                \"res.users\",\n                \"get_selected_calendars_partner_ids\",\n                [[user.userId], filters[\"user\"]]\n            );\n            // Filter attendees to be shown if 'everybody' filter isn't active.\n            if (!filters[\"all\"])\n                params.domain.push([\"partner_ids\", \"in\", selectedPartnerIds]);\n        }\n        return super.load(params);\n    }\n}\n\nexport const CalendarListView = {\n    ...listView,\n    Model: CalendarListModel,\n};\n\nfunction _mockGetCalendarPartnerIds(params) {\n    /* Mock function for when there aren't records to be shown. */\n    return [];\n}\n\nregistry.category(\"views\").add(\"calendar_list_view\", CalendarListView);\nregistry.category(\"sample_server\").add(\"get_selected_calendars_partner_ids\", _mockGetCalendarPartnerIds);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { WeekDays, weekDays } from \"@web/views/widgets/week_days/week_days\";\n\nexport class CalendarWeekDays extends WeekDays {\n    static template = \"calendar.WeekDays\";\n    onChange(day) {\n        this.props.record.update({ [day]: !this.data[day] });\n    }\n};\n\nexport const calendarWeekDays = {\n    component: CalendarWeekDays,\n    fieldDependencies: weekDays.fieldDependencies,\n};\n\nregistry.category(\"view_widgets\").add(\"calendar_week_days\", calendarWeekDays);\n", "/* @odoo-module */\n\nimport { ActivityMenu } from \"@mail/core/web/activity_menu\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(ActivityMenu.prototype, {\n    availableViews(group) {\n        if (group.model === \"crm.lead\") {\n            return [\n                [false, \"list\"],\n                [false, \"kanban\"],\n                [false, \"form\"],\n                [false, \"calendar\"],\n                [false, \"pivot\"],\n                [false, \"graph\"],\n                [false, \"activity\"],\n            ];\n        }\n        return super.availableViews(...arguments);\n    },\n\n    openActivityGroup(group, filter = \"all\") {\n        // fetch the data from the button otherwise fetch the ones from the parent (.o_ActivityMenuView_activityGroup).\n        const context = {};\n        if (group.model === \"crm.lead\") {\n            this.dropdown.close();\n            if (filter === \"my\") {\n                context[\"search_default_activities_overdue\"] = 1;\n                context[\"search_default_activities_today\"] = 1;\n            } else {\n                context[\"search_default_activities_\" + filter] = 1;\n            }\n            // Necessary because activity_ids of mail.activity.mixin has auto_join\n            // So, duplicates are faking the count and \"Load more\" doesn't show up\n            context[\"force_search_count\"] = 1;\n            this.action.doAction(\"crm.crm_lead_action_my_activities\", {\n                additionalContext: context,\n                clearBreadcrumbs: true,\n            });\n        } else {\n            return super.openActivityGroup(group, filter);\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('crm_tour', {\n    url: \"/odoo\",\n    steps: () => [stepUtils.showAppsMenuItem(), {\n    isActive: [\"community\"],\n    trigger: '.o_app[data-menu-xmlid=\"crm.crm_menu_root\"]',\n    content: markup(_t('Ready to boost your sales? Let\\'s have a look at your <b>Pipeline</b>.')),\n    tooltipPosition: 'bottom',\n    run: \"click\",\n}, {\n    isActive: [\"enterprise\"],\n    trigger: '.o_app[data-menu-xmlid=\"crm.crm_menu_root\"]',\n    content: markup(_t('Ready to boost your sales? Let\\'s have a look at your <b>Pipeline</b>.')),\n    tooltipPosition: 'bottom',\n    run: \"click\",\n},\n{\n    trigger: \".o_opportunity_kanban\",\n},\n{\n    trigger: '.o_opportunity_kanban .o-kanban-button-new',\n    content: markup(_t(\"<b>Create your first opportunity.</b>\")),\n    tooltipPosition: 'bottom',\n    run: \"click\",\n}, {\n    trigger: \".o_kanban_quick_create .o_field_widget[name='partner_id'] input\",\n    content: markup(_t('<b>Write a few letters</b> to look for a company, or create a new one.')),\n    tooltipPosition: \"top\",\n    run: \"edit Brandon Freeman\",\n}, {\n    isActive: [\"auto\"],\n    trigger: \".ui-menu-item > a\",\n    run: \"click\",\n}, {\n    trigger: \".o_kanban_quick_create .o_kanban_add\",\n    content: markup(_t(\"Now, <b>add your Opportunity</b> to your Pipeline.\")),\n    tooltipPosition: \"bottom\",\n    run: \"click\",\n},\n{\n    trigger: \".o_opportunity_kanban\",\n},\n{\n    trigger: \".o_opportunity_kanban .o_kanban_group:first-child .o_kanban_record:last-of-type\",\n    content: markup(_t(\"<b>Drag &amp; drop opportunities</b> between columns as you progress in your sales cycle.\")),\n    tooltipPosition: \"right\",\n    run: \"drag_and_drop(.o_opportunity_kanban .o_kanban_group:eq(2))\",\n},\n{\n    trigger: \".o_opportunity_kanban\",\n},\n{\n    // Choose the element that is not going to be moved by the previous step.\n    trigger: \".o_opportunity_kanban .o_kanban_group .o_kanban_record .o-mail-ActivityButton\",\n    content: markup(_t(\"Looks like nothing is planned. :(<br><br><i>Tip: Schedule activities to keep track of everything you have to do!</i>\")),\n    tooltipPosition: \"bottom\",\n    run: \"click\",\n},\n{\n    trigger: \".o_opportunity_kanban\",\n},\n{\n    trigger: \".o-mail-ActivityListPopover button:contains(Schedule an activity)\",\n    content: markup(_t(\"Let's <b>Schedule an Activity.</b>\")),\n    tooltipPosition: \"bottom\",\n    run: \"click\",\n}, {\n    trigger: '.modal-footer button[name=\"action_schedule_activities\"]',\n    content: markup(_t(\"All set. Let\u2019s <b>Schedule</b> it.\")),\n    tooltipPosition: \"top\",  // dot NOT move to bottom, it would cause a resize flicker, see task-2476595\n    run: \"click\",\n}, {\n    id: \"drag_opportunity_to_won_step\",\n    trigger: \".o_opportunity_kanban .o_kanban_record:last-of-type\",\n    content: markup(_t(\"Drag your opportunity to <b>Won</b> when you get the deal. Congrats!\")),\n    tooltipPosition: \"right\",\n    run: \"drag_and_drop(.o_opportunity_kanban .o_kanban_group:eq(3))\",\n},\n{\n    trigger: \".o_opportunity_kanban\",\n},\n{\n    trigger: \".o_kanban_record\",\n    content: _t(\"Let\u2019s have a look at an Opportunity.\"),\n    tooltipPosition: \"right\",\n    run: \"click\",\n}, {\n    trigger: \".o_lead_opportunity_form .o_statusbar_status\",\n    content: _t(\"You can make your opportunity advance through your pipeline from here.\"),\n    tooltipPosition: \"bottom\",\n    run: \"click\",\n}, {\n    trigger: \".breadcrumb-item:not(.active):first\",\n    content: _t(\"Click on the breadcrumb to go back to your Pipeline. Odoo will save all modifications as you navigate.\"),\n    tooltipPosition: \"bottom\",\n    run: \"click .breadcrumb-item:not(.active):last\",\n}]});\n", "/** @odoo-module **/\n\nexport async function checkRainbowmanMessage(orm, effect, recordId) {\n    const message = await orm.call(\"crm.lead\", \"get_rainbowman_message\", [[recordId]]);\n    if (message) {\n        effect.add({\n            message,\n            type: \"rainbow_man\",\n        });\n    }\n}\n", "/** @odoo-module **/\n\nimport { checkRainbowmanMessage } from \"@crm/views/check_rainbowman_message\";\nimport { registry } from \"@web/core/registry\";\nimport { formView } from \"@web/views/form/form_view\";\n\nclass CrmFormRecord extends formView.Model.Record {\n     /**\n     * override of record _save mechanism intended to affect the main form record\n     * We check if the stage_id field was altered and if we need to display a rainbowman\n     * message.\n     *\n     * This method will also simulate a real \"force_save\" on the email and phone\n     * when needed. The \"force_save\" attribute only works on readonly field. For our\n     * use case, we need to write the email and the phone even if the user didn't\n     * change them, to synchronize those values with the partner (so the email / phone\n     * inverse method can be called).\n     *\n     * We base this synchronization on the value of \"partner_phone_update\"\n     * and \"partner_email_update\", which are computed fields that hold a value\n     * whenever we need to synch.\n     *\n     * @override\n     */\n    async _save() {\n        if (this.resModel !== \"crm.lead\") {\n            return super._save(...arguments);\n        }\n        let changeStage = false;\n        const needsSynchronizationEmail =\n            this._changes.partner_email_update === undefined\n                ? this._values.partner_email_update // original value\n                : this._changes.partner_email_update; // new value\n\n        const needsSynchronizationPhone =\n            this._changes.partner_phone_update === undefined\n                ? this._values.partner_phone_update // original value\n                : this._changes.partner_phone_update; // new value\n\n        if (needsSynchronizationEmail && this._changes.email_from === undefined && this._values.email_from) {\n            this._changes.email_from = this._values.email_from;\n        }\n        if (needsSynchronizationPhone && this._changes.phone === undefined && this._values.phone) {\n            this._changes.phone = this._values.phone;\n        }\n\n        if (\"stage_id\" in this._changes) {\n            changeStage = this._values.stage_id !== this.data.stage_id;\n        }\n\n        const res = await super._save(...arguments);\n        if (changeStage) {\n            await checkRainbowmanMessage(this.model.orm, this.model.effect, this.resId);\n        }\n        return res;\n    }\n}\n\nclass CrmFormModel extends formView.Model {\n    static Record = CrmFormRecord;\n    static services = [...formView.Model.services, \"effect\"];\n\n    setup(params, services) {\n        super.setup(...arguments);\n        this.effect = services.effect;\n    }\n}\n\nregistry.category(\"views\").add(\"crm_form\", {\n    ...formView,\n    Model: CrmFormModel,\n});\n", "/** @odoo-module */\n\nimport { onWillStart } from \"@odoo/owl\";\nimport { user } from \"@web/core/user\";\nimport { ColumnProgress } from \"@web/views/view_components/column_progress\";\nimport { session } from \"@web/session\";\nimport { getCurrency } from \"@web/core/currency\";\n\nexport class CrmColumnProgress extends ColumnProgress {\n    static props = {\n        ...ColumnProgress.props,\n        progressBarState: { type: Object },\n    };\n    static template = \"crm.ColumnProgress\";\n    setup() {\n        super.setup();\n        this.showRecurringRevenue = false;\n\n        onWillStart(async () => {\n            if (this.props.progressBarState.progressAttributes.recurring_revenue_sum_field) {\n                this.showRecurringRevenue = await user.hasGroup(\"crm.group_use_recurring_revenues\");\n            }\n        });\n    }\n\n    getRecurringRevenueGroupAggregate(group) {\n        const rrField = this.props.progressBarState.progressAttributes.recurring_revenue_sum_field;\n        const aggregatedValue = this.props.progressBarState.getAggregateValue(group, rrField);\n        let currency = false;\n        if (aggregatedValue.value && rrField.currency_field) {\n            currency = getCurrency(session.company_currency_id);\n        }\n        return { ...aggregatedValue, currency };\n    }\n}\n", "/** @odoo-module **/\n\nimport { KanbanArchParser } from \"@web/views/kanban/kanban_arch_parser\";\nimport { extractAttributes } from \"@web/core/utils/xml\";\n\nexport class CrmKanbanArchParser extends KanbanArchParser {\n    /**\n     * @override\n     */\n    parseProgressBar(progressBar, fields) {\n        const result = super.parseProgressBar(...arguments);\n        const attrs = extractAttributes(progressBar, [\"recurring_revenue_sum_field\"]);\n        result.recurring_revenue_sum_field = fields[attrs.recurring_revenue_sum_field] || false;\n        return result;\n    }\n}\n", "/** @odoo-module **/\n\nimport { checkRainbowmanMessage } from \"@crm/views/check_rainbowman_message\";\nimport { RelationalModel } from \"@web/model/relational_model/relational_model\";\n\nexport class CrmKanbanModel extends RelationalModel {\n    setup(params, { effect }) {\n        super.setup(...arguments);\n        this.effect = effect;\n    }\n}\n\nexport class CrmKanbanDynamicGroupList extends RelationalModel.DynamicGroupList {\n    /**\n     * @override\n     *\n     * If the kanban view is grouped by stage_id check if the lead is won and display\n     * a rainbowman message if that's the case.\n     */\n    async moveRecord(dataRecordId, dataGroupId, refId, targetGroupId) {\n        await super.moveRecord(...arguments);\n        const sourceGroup = this.groups.find((g) => g.id === dataGroupId);\n        const targetGroup = this.groups.find((g) => g.id === targetGroupId);\n        if (\n            dataGroupId !== targetGroupId &&\n            sourceGroup &&\n            targetGroup &&\n            sourceGroup.groupByField.name === \"stage_id\"\n        ) {\n            const record = targetGroup.list.records.find((r) => r.id === dataRecordId);\n            await checkRainbowmanMessage(this.model.orm, this.model.effect, record.resId);\n        }\n    }\n}\n\nCrmKanbanModel.DynamicGroupList = CrmKanbanDynamicGroupList;\nCrmKanbanModel.services = [...RelationalModel.services, \"effect\"];\n", "/** @odoo-module **/\n\nimport { CrmColumnProgress } from \"./crm_column_progress\";\nimport { KanbanRenderer } from \"@web/views/kanban/kanban_renderer\";\nimport { KanbanHeader } from \"@web/views/kanban/kanban_header\";\n\nclass CrmKanbanHeader extends KanbanHeader {\n    static template = \"crm.CrmKanbanHeader\";\n    static components = {\n        ...KanbanHeader.components,\n        ColumnProgress: CrmColumnProgress,\n    };\n}\n\nexport class CrmKanbanRenderer extends KanbanRenderer {\n    static components = {\n        ...KanbanRenderer.components,\n        KanbanHeader: CrmKanbanHeader,\n    };\n}\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { CrmKanbanModel } from \"@crm/views/crm_kanban/crm_kanban_model\";\nimport { CrmKanbanArchParser } from \"@crm/views/crm_kanban/crm_kanban_arch_parser\";\nimport { CrmKanbanRenderer } from \"@crm/views/crm_kanban/crm_kanban_renderer\";\n\nexport const crmKanbanView = {\n    ...kanbanView,\n    ArchParser: CrmKanbanArchParser,\n    // Makes it easier to patch\n    Controller: class extends kanbanView.Controller {\n        get progressBarAggregateFields() {\n            const res = super.progressBarAggregateFields;\n            const progressAttributes = this.props.archInfo.progressAttributes;\n            if (progressAttributes && progressAttributes.recurring_revenue_sum_field) {\n                res.push(progressAttributes.recurring_revenue_sum_field);\n            }\n            return res;\n        }\n    },\n    Model: CrmKanbanModel,\n    Renderer: CrmKanbanRenderer,\n};\n\nregistry.category(\"views\").add(\"crm_kanban\", crmKanbanView);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport {\n    serializeDate,\n    serializeDateTime,\n} from \"@web/core/l10n/dates\";\n\n/**\n * Configuration depending on the granularity, using Luxon DateTime objects:\n * @param {function} startOf function to get a DateTime at the beginning of a period\n *                           from another DateTime.\n * @param {int} cycle amount of 'granularity' periods constituting a cycle. The cycle duration\n *                    is arbitrary for each granularity:\n * cycle    ---    granularity\n * ___________________________\n * 1 day           hour\n * 1 week          day\n * 1 week          week    # there is no greater time period that takes an integer amount of weeks\n * 1 year          month\n * 1 year          quarter\n * 1 year          year    # we are not using a greater time period in Odoo (yet)\n * @param {int} cyclePos function to get the position (index) in the cycle from a DateTime.\n *                       {1} is the first index. {+1} is used for properties which have an index\n *                       starting from 0, to standardize between granularities.\n */\nexport const GRANULARITY_TABLE = {\n    hour: {\n        startOf: (x) => x.startOf(\"hour\"),\n        cycle: 24,\n        cyclePos: (x) => x.hour + 1,\n    },\n    day: {\n        startOf: (x) => x.startOf(\"day\"),\n        cycle: 7,\n        cyclePos: (x) => x.weekday,\n    },\n    week: {\n        startOf: (x) => x.startOf(\"week\"),\n        cycle: 1,\n        cyclePos: (x) => 1,\n    },\n    month: {\n        startOf: (x) => x.startOf(\"month\"),\n        cycle: 12,\n        cyclePos: (x) => x.month,\n    },\n    quarter: {\n        startOf: (x) => x.startOf(\"quarter\"),\n        cycle: 4,\n        cyclePos: (x) => x.quarter,\n    },\n    year: {\n        startOf: (x) => x.startOf(\"year\"),\n        cycle: 1,\n        cyclePos: (x) => 1,\n    },\n};\n\n/**\n * fill_temporal period:\n *   Represents a specific date/time range for a specific model, field and granularity.\n *\n * It is used to add new domain and context constraints related to a specific date/time\n * field, in order to configure the _read_group_fill_temporal (see core models.py)\n * method. It will be used when we want to get continuous groups in chronological\n * order in a specific date/time range.\n */\nexport class FillTemporalPeriod {\n    /**\n     * This constructor is meant to be used only by the FillTemporalService (see below)\n     *\n     * @param {string} modelName directly taken from model.loadParams.modelName.\n     *                           this is the `res_model` from the action (i.e. `crm.lead`)\n     * @param {Object} field a dictionary with keys \"name\" and \"type\".\n     *                        name: Name of the field on which the fill_temporal should apply\n     *                              (i.e. 'date_deadline')\n     *                        type: 'date' or 'datetime'\n     * @param {string} granularity can either be : hour, day, week, month, quarter, year\n     * @param {integer} minGroups minimum amount of groups to display, regardless of other\n     *                            constraints\n     */\n    constructor(modelName, field, granularity, minGroups) {\n        this.modelName = modelName;\n        this.field = field;\n        this.granularity = granularity || \"month\";\n        this.setMinGroups(minGroups);\n\n        this._computeStart();\n        this._computeEnd();\n    }\n    /**\n     * Compute this.start: the DateTime for the start of the period containing\n     * the current time (\"now\").\n     * i.e. 2020-10-01 13:43:17 -> the current \"hour\" DateTime started at:\n     *      2020-10-01 13:00:00\n     *\n     * @private\n     */\n    _computeStart() {\n        this.start = GRANULARITY_TABLE[this.granularity].startOf(luxon.DateTime.now());\n    }\n    /**\n     * Compute this.end: the DateTime for the end of the fill_temporal period.\n     * This bound is exclusive.\n     * The fill_temporal period is the number of [granularity] from [start] to the end of the\n     * [cycle] reached after adding [minGroups]\n     * i.e. we are in october 2020 :\n     *      [start] = 2020-10-01\n     *      [granularity] = 'month',\n     *      [cycle] = 12\n     *      [minGroups] = 4,\n     *      => fillTemporalPeriod = 15 months (until end of december 2021)\n     *\n     * @private\n     */\n    _computeEnd() {\n        const cycle = GRANULARITY_TABLE[this.granularity].cycle;\n        const cyclePos = GRANULARITY_TABLE[this.granularity].cyclePos(this.start);\n        // fillTemporalPeriod formula explanation :\n        // We want to know how many steps need to be taken from the current position until the end\n        // of the cycle reached after guaranteeing minGroups positions. Let's call this cycle (C).\n        //\n        // (1) compute the steps needed to reach the last position of the current cycle, from the\n        //     current position:\n        //     {cycle - cyclePos}\n        //\n        // (2) ignore {minGroups - 1} steps from the position reached in (1). Now, the current\n        //     position is somewhere in (C). One step from minGroups is reserved to reach the first\n        //     position after (C), hence {-1}\n        //\n        // (3) compute the additional steps needed to reach the last position of (C), from the\n        //     position reached in (2):\n        //     {cycle - (minGroups - 1) % cycle}\n        //\n        // (4) combine (1) and (3), the sum should not be greater than a full cycle (-> truncate):\n        //     {(2 * cycle - (minGroups - 1) % cycle - cyclePos) % cycle}\n        //\n        // (5) add minGroups!\n        const fillTemporalPeriod = ((2 * cycle - ((this.minGroups - 1) % cycle) - cyclePos) % cycle) + this.minGroups;\n        this.end = this.start.plus({[`${this.granularity}s`]: fillTemporalPeriod});\n        this.computedEnd = true;\n    }\n    /**\n     * The server needs a date/time in UTC, but we don't want a day shift in case\n     * of dates, even if the date is not in UTC\n     *\n     * @param {DateTime} bound the DateTime to be formatted (this.start or this.end)\n     */\n    _getFormattedServerDate(bound) {\n        if (this.field.type === \"date\") {\n            return serializeDate(bound);\n        } else {\n            return serializeDateTime(bound);\n        }\n    }\n    /**\n     * @param {Object} configuration\n     * @param {Array[]} [domain]\n     * @param {boolean} [forceStartBound=true] whether this.start DateTime must be used as a domain\n     *                                         constraint to limit read_group results or not\n     * @param {boolean} [forceEndBound=true] whether this.end DateTime must be used as a domain\n     *                                       constraint to limit read_group results or not\n     * @returns {Array[]} new domain\n     */\n    getDomain({ domain, forceStartBound = true, forceEndBound = true }) {\n        if (!forceEndBound && !forceStartBound) {\n            return domain;\n        }\n        const originalDomain = domain.length ? [\"&\", ...domain] : [];\n        const defaultDomain = [\"|\", [this.field.name, \"=\", false]];\n        const linkDomain = forceStartBound && forceEndBound ? [\"&\"] : [];\n        const startDomain = !forceStartBound ? [] : [[this.field.name, \">=\", this._getFormattedServerDate(this.start)]];\n        const endDomain = !forceEndBound ? [] : [[this.field.name, \"<\", this._getFormattedServerDate(this.end)]];\n        return [...originalDomain, ...defaultDomain, ...linkDomain, ...startDomain, ...endDomain];\n    }\n    /**\n     * The default value of forceFillingTo is false when this.end is the\n     * computed one, and true when it is manually set. This is because the default value of\n     * this.end is computed without any knowledge of the existing data, and as such, we only\n     * want to get continuous groups until the last group with data (no need to force until\n     * this.end). On the contrary, when we set this.end, this means that we want groups until\n     * that date.\n     *\n     * @param {Object} configuration\n     * @param {Object} [context]\n     * @param {boolean} [forceFillingFrom=true] fill_temporal must apply from:\n     *                                          true: this.start\n     *                                          false: the first group with at least one record\n     * @param {boolean} [forceFillingTo=!this.computedEnd] fill_temporal must apply until:\n     *                                          true: this.end\n     *                                          false: the last group with at least one record\n     * @returns {Object} new context\n     */\n    getContext({ context, forceFillingFrom = true, forceFillingTo = !this.computedEnd }) {\n        const fillTemporal = {\n            min_groups: this.minGroups,\n        };\n        if (forceFillingFrom) {\n            fillTemporal.fill_from = this._getFormattedServerDate(this.start);\n        }\n        if (forceFillingTo) {\n            // smallest time interval used in Odoo for the current date type\n            const minGranularity = this.field.type === \"date\" ? \"days\" : \"seconds\";\n            fillTemporal.fill_to = this._getFormattedServerDate(this.end.minus({[minGranularity]: 1}));\n        }\n        context = { ...context, fill_temporal: fillTemporal };\n        return context;\n    }\n    /**\n     * @param {integer} minGroups minimum amount of groups to display, regardless of other\n     *                            constraints\n     */\n    setMinGroups(minGroups) {\n        this.minGroups = minGroups || 1;\n    }\n    /**\n     * sets the end of the period to the desired DateTime. It must be greater\n     * than start. Changes the default behavior of getContext forceFillingTo\n     * (becomes true instead of false)\n     *\n     * @param {DateTime} end\n     */\n    setEnd(end) {\n        this.end = luxon.DateTime.max(this.start, end);\n        this.computedEnd = false;\n    }\n    /**\n     * sets the start of the period to the desired DateTime. It must be smaller than end\n     *\n     * @param {DateTime} start\n     */\n    setStart(start) {\n        this.start = luxon.DateTime.min(this.end, start);\n    }\n    /**\n     * Adds one \"granularity\" period to [this.end], to expand the current fill_temporal period\n     */\n    expand() {\n        this.setEnd(this.end.plus({[`${this.granularity}s`]: 1}));\n    }\n}\n\n/**\n * fill_temporal Service\n *\n * This service will be used to generate or retrieve fill_temporal periods\n *\n * A specific fill_temporal period configuration will always refer to the same instance\n * unless forceRecompute is true\n */\nexport const fillTemporalService = {\n    start() {\n        const _fillTemporalPeriods = {};\n\n        /**\n         * Get a fill_temporal period according to the configuration.\n         * The default initial fill_temporal period is the number of [granularity] from [start]\n         * to the end of the [cycle] reached after adding [minGroups]\n         * i.e. we are in october 2020 :\n         *      [start] = 2020-10-01\n         *      [granularity] = 'month',\n         *      [cycle] = 12 (one year)\n         *      [minGroups] = 4,\n         *      => fillTemporalPeriod = 15 months (until the end of december 2021)\n         * Once created, a fill_temporal period for a specific configuration will be stored\n         * until requested again. This allows to manipulate the period and store the changes\n         * to it. This also allows to keep the configuration when switching to another view\n         *\n         * @param {Object} configuration\n         * @param {string} [modelName] directly taken from model.loadParams.modelName.\n         *                             this is the `res_model` from the action (i.e. `crm.lead`)\n         * @param {Object} [field] a dictionary with keys \"name\" and \"type\".\n         * @param {string} [field.name] name of the field on which the fill_temporal should apply\n         *                              (i.e. 'date_deadline')\n         * @param {string} [field.type] date field type: 'date' or 'datetime'\n         * @param {string} [granularity] can either be : hour, day, week, month, quarter, year\n         * @param {integer} [minGroups=4] optional minimal amount of desired groups\n         * @param {boolean} [forceRecompute=false] optional whether the fill_temporal period should be\n         *                                         reinstancied\n         * @returns {FillTemporalPeriod}\n         */\n        const getFillTemporalPeriod = ({ modelName, field, granularity, minGroups = 4, forceRecompute = false }) => {\n            if (!(modelName in _fillTemporalPeriods)) {\n                _fillTemporalPeriods[modelName] = {};\n            }\n            if (!(field.name in _fillTemporalPeriods[modelName])) {\n                _fillTemporalPeriods[modelName][field.name] = {};\n            }\n            if (!(granularity in _fillTemporalPeriods[modelName][field.name]) || forceRecompute) {\n                _fillTemporalPeriods[modelName][field.name][granularity] = new FillTemporalPeriod(\n                    modelName,\n                    field,\n                    granularity,\n                    minGroups\n                );\n            } else if (_fillTemporalPeriods[modelName][field.name][granularity].minGroups != minGroups) {\n                _fillTemporalPeriods[modelName][field.name][granularity].setMinGroups(minGroups);\n            }\n            return _fillTemporalPeriods[modelName][field.name][granularity];\n        };\n        return { getFillTemporalPeriod };\n    },\n};\n\nregistry.category(\"services\").add(\"fillTemporalService\", fillTemporalService);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { INTERVAL_OPTIONS } from \"@web/search/utils/dates\";\nimport { KanbanColumnQuickCreate } from \"@web/views/kanban/kanban_column_quick_create\";\n\nexport class ForecastKanbanColumnQuickCreate extends KanbanColumnQuickCreate {\n    /**\n     * @override\n     */\n    get relatedFieldName() {\n        const { granularity = \"month\" } = this.props.groupByField;\n        const { description } = INTERVAL_OPTIONS[granularity];\n        return _t(\"Add next %s\", description.toLocaleLowerCase());\n    }\n    /**\n     * @override\n     *\n     * Create column directly upon \"unfolding\" quick create.\n     */\n    unfold() {\n        this.props.onValidate();\n    }\n}\n", "/** @odoo-module **/\n\nimport { crmKanbanView } from \"@crm/views/crm_kanban/crm_kanban_view\";\n\nexport class ForecastKanbanController extends crmKanbanView.Controller {\n    isQuickCreateField(field) {\n        return super.isQuickCreateField(...arguments) || (field && field.name === \"date_deadline\");\n    }\n}\n", "/** @odoo-module **/\n\nimport { CrmKanbanModel } from \"@crm/views/crm_kanban/crm_kanban_model\";\nimport { deserializeDateTime } from \"@web/core/l10n/dates\";\n\nexport class ForecastKanbanModel extends CrmKanbanModel {\n    setup(params, { fillTemporalService }) {\n        super.setup(...arguments);\n        this.fillTemporalService = fillTemporalService;\n        this.forceNextRecompute = !params.state?.groups;\n        this.originalDomain = null;\n        this.fillTemporalDomain = null;\n    }\n\n    async _webReadGroup(config, orderBy) {\n        if (this.isForecastGroupBy(config)) {\n            config.context = this.fillTemporalPeriod(config).getContext({\n                context: config.context,\n            });\n            // Domain leaves added by the fillTemporalPeriod should be replaced\n            // between 2 _webReadGroup calls, not added on top of each other.\n            // Keep track of the modified domain, and if encountered in the\n            // future, modify the original domain instead. It is not robust\n            // against external modification of `config.domain`, but currently\n            // there are only replacements except this case.\n            if (!this.originalDomain || this.fillTemporalDomain !== config.domain) {\n                this.originalDomain = config.domain || [];\n            }\n            this.fillTemporalDomain = this.fillTemporalPeriod(config).getDomain({\n                domain: this.originalDomain,\n                forceStartBound: false,\n            });\n            config.domain = this.fillTemporalDomain;\n        }\n        return super._webReadGroup(...arguments);\n    }\n\n    async _loadGroupedList(config) {\n        const res = await super._loadGroupedList(...arguments);\n        if (this.isForecastGroupBy(config)) {\n            const lastGroup = res.groups.filter((grp) => grp.value).slice(-1)[0];\n            if (lastGroup) {\n                this.fillTemporalPeriod(config).setEnd(deserializeDateTime(lastGroup.range.to));\n            }\n        }\n        return res;\n    }\n\n    /**\n     * @returns {Boolean} true if the view is grouped by the forecast_field\n     */\n    isForecastGroupBy(config) {\n        const forecastField = config.context.forecast_field;\n        const name = config.groupBy[0].split(\":\")[0];\n        return forecastField && forecastField === name;\n    }\n\n    /**\n     * return {FillTemporalPeriod} current fillTemporalPeriod according to group by state\n     */\n    fillTemporalPeriod(config) {\n        const [groupByFieldName, granularity] = config.groupBy[0].split(\":\");\n        const groupByField = config.fields[groupByFieldName];\n        const minGroups = (config.context.fill_temporal && config.context.fill_temporal.min_groups) || undefined;\n        const { name, type } = groupByField;\n        const forceRecompute = this.forceNextRecompute;\n        this.forceNextRecompute = false;\n        return this.fillTemporalService.getFillTemporalPeriod({\n            modelName: config.resModel,\n            field: {\n                name,\n                type,\n            },\n            granularity: granularity || \"month\",\n            minGroups,\n            forceRecompute,\n        });\n    }\n}\n\nForecastKanbanModel.services = [...CrmKanbanModel.services, \"fillTemporalService\"];\n", "/** @odoo-module **/\n\nimport { CrmKanbanRenderer } from \"@crm/views/crm_kanban/crm_kanban_renderer\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { ForecastKanbanColumnQuickCreate } from \"@crm/views/forecast_kanban/forecast_kanban_column_quick_create\";\n\nexport class ForecastKanbanRenderer extends CrmKanbanRenderer {\n    static template = \"crm.ForecastKanbanRenderer\";\n    static components = {\n        ...CrmKanbanRenderer.components,\n        ForecastKanbanColumnQuickCreate,\n    };\n\n    setup() {\n        super.setup(...arguments);\n        this.fillTemporalService = useService(\"fillTemporalService\");\n    }\n    /**\n     * @override\n     *\n     * Allow creating groups when grouping by forecast_field.\n     */\n    canCreateGroup() {\n        return super.canCreateGroup(...arguments) || this.isGroupedByForecastField();\n    }\n\n    isGroupedByForecastField() {\n        return (\n            this.props.list.context.forecast_field &&\n            this.props.list.groupByField.name === this.props.list.context.forecast_field\n        );\n    }\n\n    isMovableField(field) {\n        return super.isMovableField(...arguments) || field.name === \"date_deadline\";\n    }\n\n    async addForecastColumn() {\n        const { name, type, granularity } = this.props.list.groupByField;\n        this.fillTemporalService\n            .getFillTemporalPeriod({\n                modelName: this.props.list.resModel,\n                field: {\n                    name,\n                    type,\n                },\n                granularity: granularity || \"month\",\n            })\n            .expand();\n        await this.props.list.load();\n    }\n}\n", "/** @odoo-module **/\n\nimport { ForecastKanbanController } from \"@crm/views/forecast_kanban/forecast_kanban_controller\";\nimport { CrmKanbanArchParser } from \"@crm/views/crm_kanban/crm_kanban_arch_parser\";\nimport { ForecastKanbanModel } from \"@crm/views/forecast_kanban/forecast_kanban_model\";\nimport { ForecastKanbanRenderer } from \"@crm/views/forecast_kanban/forecast_kanban_renderer\";\nimport { ForecastSearchModel } from \"@crm/views/forecast_search_model\";\nimport { registry } from \"@web/core/registry\";\nimport { kanbanView } from \"@web/views/kanban/kanban_view\";\n\nexport const forecastKanbanView = {\n    ...kanbanView,\n    ArchParser: CrmKanbanArchParser,\n    Model: ForecastKanbanModel,\n    Controller: ForecastKanbanController,\n    Renderer: ForecastKanbanRenderer,\n    SearchModel: ForecastSearchModel,\n};\n\nregistry.category(\"views\").add(\"forecast_kanban\", forecastKanbanView);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { listView } from \"@web/views/list/list_view\";\nimport { ForecastSearchModel } from \"@crm/views/forecast_search_model\";\n\nexport const forecastListView = {\n    ...listView,\n    SearchModel: ForecastSearchModel,\n};\n\nregistry.category(\"views\").add(\"forecast_list\", forecastListView);\n", "/** @odoo-module **/\n\nimport { Domain } from \"@web/core/domain\";\nimport { makeContext } from \"@web/core/context\";\nimport { SearchModel } from \"@web/search/search_model\";\nimport {\n    serializeDate,\n    serializeDateTime,\n} from \"@web/core/l10n/dates\";\n\n/**\n * This is the conversion of ForecastModelExtension. See there for more\n * explanations of what is done here.\n */\n\nexport class ForecastSearchModel extends SearchModel {\n    /**\n     * @override\n     */\n    exportState() {\n        const state = super.exportState();\n        state.forecast = {\n            forecastStart: this.forecastStart,\n        };\n        return state;\n    }\n\n    /**\n     * @override\n     */\n    _getSearchItemDomain(activeItem) {\n        let domain = super._getSearchItemDomain(activeItem);\n        const { searchItemId } = activeItem;\n        const searchItem = this.searchItems[searchItemId];\n        const context = makeContext([searchItem.context || {}]);\n        if (context.forecast_filter) {\n            const forecastField = this.globalContext.forecast_field;\n            const forecastStart = this._getForecastStart(forecastField);\n            const forecastDomain = [\n                \"|\",\n                [forecastField, \"=\", false],\n                [forecastField, \">=\", forecastStart],\n            ];\n            domain = Domain.and([domain, forecastDomain]);\n        }\n        return domain;\n    }\n\n    /**\n     * @protected\n     * @param {string} forecastField\n     * @returns {string}\n     */\n    _getForecastStart(forecastField) {\n        if (!this.forecastStart) {\n            const { type } = this.searchViewFields[forecastField];\n            const groupBy = this.groupBy;\n            const firstForecastGroupBy = groupBy.find((gb) => gb.includes(forecastField));\n            let granularity = \"month\";\n            if (firstForecastGroupBy) {\n                granularity = firstForecastGroupBy.split(\":\")[1] || \"month\";\n            } else if (groupBy.length) {\n                granularity = \"day\";\n            }\n            const startDateTime = luxon.DateTime.now().startOf(granularity);\n            this.forecastStart = type === \"datetime\" ? serializeDateTime(startDateTime) : serializeDate(startDateTime);\n        }\n        return this.forecastStart;\n    }\n\n    /**\n     * @override\n     */\n    _importState(state) {\n        super._importState(...arguments);\n        if (state.forecast) {\n            this.forecastStart = state.forecast.forecastStart;\n        }\n    }\n\n    /**\n     * @override\n     */\n    _reset() {\n        super._reset();\n        this.forecastStart = null;\n    }\n}\n", "/** @odoo-module */\n\nimport { CodeEditor } from \"@web/core/code_editor/code_editor\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { CheckboxItem } from \"@web/core/dropdown/checkbox_item\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { SelectMenu } from \"@web/core/select_menu/select_menu\";\nimport { user } from \"@web/core/user\";\nimport { sortBy } from \"@web/core/utils/arrays\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { ResourceEditorWarningOverlay } from \"./resource_editor_warning\";\nimport { checkSCSS, checkXML, formatXML } from \"./utils\";\n\nimport { Component, onWillUnmount, onWillStart, reactive, useRef, useState } from \"@odoo/owl\";\n\nconst BUNDLES_RESTRICTION = [\n    \"web.assets_frontend\",\n    \"web.assets_frontend_minimal\",\n    \"web.assets_frontend_lazy\",\n];\n\nexport class ResourceEditor extends Component {\n    static components = {\n        ResourceEditorWarningOverlay,\n        CodeEditor,\n        Dropdown,\n        CheckboxItem,\n        DropdownItem,\n        SelectMenu,\n    };\n    static template = \"website.ResourceEditor\";\n    static props = {\n        close: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        close: () => {},\n    };\n\n    setup() {\n        this.website = useService(\"website\");\n        this.orm = useService(\"orm\");\n        this.dialog = useService(\"dialog\");\n\n        this.keepLast = new KeepLast();\n\n        this.editorRef = useRef(\"editor\");\n\n        this.debug = this.env.debug;\n        this.viewKey =\n            this.website.pageDocument &&\n            this.website.pageDocument.documentElement.dataset.viewXmlid;\n\n        this.types = {\n            xml: \"XML (HTML)\",\n            scss: \"SCSS (CSS)\",\n            js: \"JS\",\n        };\n        this.typeToCodeEditorModeMap = {\n            xml: \"qweb\",\n            scss: \"scss\",\n            js: \"javascript\",\n        };\n\n        this.xmlFilters = {\n            views: _t(\"Only Views\"),\n            all: _t(\"Views and Assets bundles\"),\n        };\n        this.scssFilters = {\n            custom: _t(\"Only Custom SCSS Files\"),\n            restricted: _t(\"Only Page SCSS Files\"),\n            all: _t(\"All SCSS Files\"),\n        };\n        this.state = useState({\n            type: \"xml\",\n            xmlFilter: \"views\",\n            scssFilter: \"custom\",\n            currentResource: false,\n            showEditWarning: true,\n            resources: {\n                xml: {},\n                js: {},\n                scss: {},\n            },\n            sortedXML: [],\n            sortedSCSS: [],\n            sortedJS: [],\n            saving: false,\n        });\n\n        let showErrorInterval;\n        this.errors = reactive([], () => {\n            clearInterval(showErrorInterval);\n            if (this.errors.length) {\n                this.showErrorLine();\n                // The ace library updates its content asynchronously, and sometimes\n                // at unexpected moments, so we consistently re-apply the error indicators\n                // when they are errors. This is kind of a hack, but it works.\n                showErrorInterval = setInterval(() => this.showErrorLine(), 500);\n            } else {\n                this.clearErrorLine();\n            }\n        });\n        onWillUnmount(() => clearInterval(showErrorInterval));\n\n        onWillStart(async () => this.loadResources());\n    }\n\n    // -------------------------------------------------------------------------\n    // Getters\n    // -------------------------------------------------------------------------\n\n    get context() {\n        return {\n            ...user.context,\n            website_id: this.website.currentWebsite.id,\n        };\n    }\n\n    get resourceInfo() {\n        if (!this.state.currentResource) {\n            return \"\";\n        }\n        if (this.state.type === \"xml\") {\n            return _t(\"Template ID: %s\", this.state.currentResource.key);\n        } else if (this.state.type === \"scss\") {\n            return _t(\"SCSS file: %s\", this.state.currentResource.url);\n        } else {\n            return _t(\"JS file: %s\", this.state.currentResource.url);\n        }\n    }\n\n    get selectMenuProps() {\n        const props = {\n            onSelect: (value) => {\n                this.state.currentResource = this.state.resources[this.state.type][value];\n            },\n            autoSort: false,\n            required: true,\n        };\n        if (this.state.type === \"xml\") {\n            const choices = this.state.sortedXML.map((view) => {\n                return { value: view.id, label: view.label };\n            });\n            const value = this.state.currentResource?.id;\n            return { ...props, choices, value };\n        } else {\n            const { type, sortedSCSS, sortedJS } = this.state;\n            const bundles = type === \"scss\" ? sortedSCSS : sortedJS;\n            const groups = bundles.map(([name, files]) => {\n                const choices = files.map((file) => ({ value: file.url, label: file.label }));\n                return { label: name, choices };\n            });\n            const value = this.state.currentResource?.url;\n            return { ...props, groups, value };\n        }\n    }\n\n    // -------------------------------------------------------------------------\n    // Methods\n    // -------------------------------------------------------------------------\n\n    /**\n     * Checks resource is customized or not.\n     *\n     * @param {string} url\n     * @returns {boolean}\n     */\n    isCustomResource(url) {\n        // TODO we should be able to detect if the XML template is customized\n        // to not show the warning in that case\n        if (this.state.type === \"scss\") {\n            return this.state.resources.scss[url].customized;\n        } else if (this.state.type === \"js\") {\n            return this.state.resources.js[url].customized;\n        }\n        return false;\n    }\n\n    async loadResources() {\n        const resources = await this.keepLast.add(\n            rpc(\"/web_editor/get_assets_editor_resources\", {\n                key: this.viewKey,\n                bundles: this.state.xmlFilter === \"all\",\n                bundles_restriction: BUNDLES_RESTRICTION,\n                only_user_custom_files: this.state.scssFilter === \"custom\",\n            })\n        );\n        this.state.resources = { xml: {}, js: {}, scss: {} };\n        this.processResources(resources.views || [], \"xml\");\n        this.processResources(resources.scss || [], \"scss\");\n        this.processResources(resources.js || [], \"js\");\n        const type = this.state.type;\n        if (this.state.currentResource) {\n            this.state.currentResource = this.state.resources[type][this.state.currentResource.id];\n        }\n        if (!this.state.currentResource) {\n            this.setDefaultFile();\n        }\n        this.errors.length = 0;\n    }\n\n    processResources(resources, type) {\n        if (type === \"xml\") {\n            // Only keep the active views and index them by ID.\n            const indexedById = {};\n            resources\n                .filter((view) => view.active)\n                .forEach((view) => {\n                    view.type = \"xml\";\n                    indexedById[view.id] = view;\n                });\n            Object.assign(this.state.resources.xml, indexedById);\n\n            // Initialize a 0 level for each view and assign them an array containing their children.\n            const roots = [];\n            Object.values(this.state.resources.xml).forEach((view) => {\n                view.level = 0;\n                view.children = [];\n            });\n            Object.values(this.state.resources.xml).forEach((view) => {\n                const parentId = view.inherit_id[0];\n                const parent = parentId && this.state.resources.xml[parentId];\n                if (parent) {\n                    parent.children.push(view);\n                } else {\n                    roots.push(view);\n                }\n            });\n\n            // Assign the correct level based on children key and save a sorted array where\n            // each view is followed by their children.\n            const sortedXML = [];\n            const visit = (view, level) => {\n                view.level = level;\n                sortedXML.push(view);\n                view.children.forEach((child) => {\n                    visit(child, level + 1);\n                });\n            };\n            roots.forEach((root) => {\n                visit(root, 0);\n            });\n            this.state.sortedXML = sortedXML;\n\n            // Compute labels\n            Object.values(this.state.resources.xml).forEach((view) => {\n                view.label = `${\"-\".repeat(view.level)} ${view.name}`;\n                if (this.debug && view.xml_id) {\n                    view.label += ` (${view.xml_id})`;\n                }\n            });\n        } else if (type === \"scss\" || type === \"js\") {\n            // The received scss or js data is already sorted by bundle and DOM order\n            if (type === \"scss\") {\n                this.state.sortedSCSS = resources;\n            } else {\n                this.state.sortedJS = resources;\n            }\n\n            // Store the URL ungrouped by bundle and use the URL as key (resource ID)\n            resources.forEach(([bundle, files]) => {\n                const indexedByUrl = {};\n                files.forEach((file) => {\n                    // Compute labels\n                    file.label = file.url.split(\"/\").at(-1).split(\".\")[0];\n                    if (this.debug) {\n                        file.label += ` (${file.url})`;\n                    }\n\n                    file.bundle = bundle;\n                    file.id = file.url; // for consistency with xml resources\n                    file.type = type;\n                    indexedByUrl[file.url] = file;\n                });\n                if (type === \"scss\") {\n                    Object.assign(this.state.resources.scss, indexedByUrl);\n                } else {\n                    Object.assign(this.state.resources.js, indexedByUrl);\n                }\n            });\n        }\n    }\n\n    /**\n     * Forces the current scss/js file identified by its url to be reset to the way\n     * it was before the user started editing it.\n     *\n     * @todo views (xml) reset is not supported yet\n     *\n     * @returns {Promise}\n     */\n    async resetResource() {\n        if (this.state.type === \"xml\") {\n            throw new Error(_t(\"Reseting views is not supported yet\"));\n        }\n        const resource = this.state.currentResource;\n        await this.orm.call(\"web_editor.assets\", \"reset_asset\", [resource.url, resource.bundle], {\n            context: this.context,\n        });\n        await this.loadResources();\n        this.website.contentWindow.location.reload();\n    }\n\n    async saveResources() {\n        const { js, scss, xml } = this.state.resources;\n        const toSave = {\n            js: Object.values(js).filter((r) => r.dirty),\n            scss: Object.values(scss).filter((r) => r.dirty),\n            // child views first as COW on a parent would delete them\n            xml: sortBy(\n                Object.values(xml).filter((r) => r.dirty),\n                \"id\"\n            ).reverse(),\n        };\n\n        for (const [type, resources] of Object.entries(toSave)) {\n            for (let i = 0; i < resources.length; i++) {\n                const arch = resources[i].arch;\n                const { isValid, error } = type === \"xml\" ? checkXML(arch) : checkSCSS(arch);\n                if (!isValid) {\n                    this.errors.push({ error, resource: resources[i] });\n                }\n            }\n        }\n        if (this.errors.length) {\n            // switch to the first resource in error if the current has no error\n            if (\n                !this.errors\n                    .map(({ resource }) => resource.id)\n                    .includes(this.state.currentResource.id)\n            ) {\n                this.state.currentResource = this.errors[0].resource;\n                this.state.type = this.errors[0].resource.type;\n            }\n            return;\n        }\n\n        // sequentially save all resources\n        for (const [type, resources] of Object.entries(toSave)) {\n            for (const resource of resources) {\n                if (type === \"xml\") {\n                    await this.saveXML(resource);\n                } else {\n                    await this.saveSCSSorJS(resource);\n                }\n            }\n        }\n        await this.loadResources();\n        this.website.contentWindow.location.reload();\n    }\n\n    /**\n     * Saves a unique SCSS or JS file.\n     *\n     * @private\n     * @param {Object} resource a SCSS or JS file to save\n     * @return {Promise} indicates if the save is finished or if an error occured.\n     */\n    async saveSCSSorJS(resource) {\n        const { url, arch } = resource;\n        const isJSFile = String(url).endsWith(\".js\");\n        const bundle = isJSFile\n            ? this.state.resources.js[url].bundle\n            : this.state.resources.scss[url].bundle;\n        const fileType = isJSFile ? \"js\" : \"scss\";\n        const params = [url, bundle, arch, fileType];\n        await this.orm.call(\"web_editor.assets\", \"save_asset\", params, { context: this.context });\n        delete resource.dirty;\n    }\n\n    /**\n     * Saves a unique XML view.\n     *\n     * @param {Object} resource an xml view to save\n     * @returns {Promise} indicates if the save is finished or if an error occured.\n     */\n    async saveXML(resource) {\n        const { id, arch } = resource;\n        await rpc(\"/website/save_xml\", {\n            view_id: id,\n            arch: arch,\n        })\n        delete resource.dirty;\n    }\n\n    setDefaultFile() {\n        if (this.state.type === \"xml\") {\n            const views = Object.values(this.state.resources.xml);\n            let view = views.find((view) => [view.id, view.xml_id].includes(this.viewKey));\n            if (!view) {\n                view = views.find((view) => view.key === this.viewKey);\n            }\n            this.state.currentResource = view || this.state.sortedXML[0] || false;\n        } else if (this.state.type === \"scss\") {\n            // By default show the user_custom_rules.scss one as some people\n            // would write rules in user_custom_bootstrap_overridden.scss\n            // otherwise, not reading the comment inside explaining how that\n            // file should be used.\n            this.state.currentResource =\n                this.state.resources.scss[\"/website/static/src/scss/user_custom_rules.scss\"];\n        } else {\n            this.state.currentResource =\n                this.state.sortedJS.map(([_, files]) => files).flat()[0] || false;\n        }\n    }\n\n    showErrorLine() {\n        const resourceId = this.state.currentResource.id;\n        const error = this.errors.find(({ resource }) => resource.id === resourceId)?.error;\n        if (error) {\n            const { line, message } = error;\n            const gutterCell = this.editorRef.el.querySelectorAll(\".ace_gutter-cell\")[line - 1];\n            if (gutterCell && !gutterCell.classList.contains(\"o_error\")) {\n                gutterCell.classList.add(\"o_error\");\n                gutterCell.setAttribute(\"data-tooltip\", message);\n                gutterCell.setAttribute(\"data-tooltip-position\", \"left\");\n            }\n        }\n    }\n\n    clearErrorLine() {\n        const allGutterCells = this.editorRef.el.querySelectorAll(\".ace_gutter-cell\");\n        for (const gutterCell of allGutterCells) {\n            gutterCell.classList.remove(\"o_error\");\n            gutterCell.removeAttribute(\"data-tooltip\");\n            gutterCell.removeAttribute(\"data-tooltip-position\");\n        }\n    }\n\n    // -------------------------------------------------------------------------\n    // Handlers\n    // -------------------------------------------------------------------------\n\n    onEditorChange(value) {\n        const currentResource = this.state.currentResource;\n        currentResource.arch = value;\n        currentResource.dirty = true;\n        this.errors.length = 0;\n    }\n\n    /**\n     * @param {\"xml\"|\"scss\"|\"js\"} type\n     */\n    onFileTypeChange(type) {\n        if (type !== this.state.type) {\n            this.state.type = type;\n            this.setDefaultFile();\n        }\n    }\n\n    /**\n     * @param {\"xml\"|\"scss\"} type\n     * @param {string} filter\n     */\n    onFilterChange(type, filter) {\n        if (type === \"scss\") {\n            this.state.scssFilter = filter;\n        } else if (type === \"xml\") {\n            this.state.xmlFilter = filter;\n        }\n        this.loadResources();\n    }\n\n    onFormat() {\n        if (this.state.type === \"xml\") {\n            const { isValid, error } = checkXML(this.state.currentResource.arch);\n            if (isValid) {\n                this.state.currentResource.arch = formatXML(this.state.currentResource.arch);\n            } else {\n                this.errors.push({ error, resource: this.state.currentResource });\n            }\n        }\n    }\n\n    onReset() {\n        this.dialog.add(ConfirmationDialog, {\n            title: _t(\"Careful\"),\n            body: _t(\n                \"If you reset this file, all your customizations will be lost as it will be reverted to the default file.\"\n            ),\n            confirm: () => this.resetResource(),\n            cancel: () => {},\n        });\n    }\n\n    async onSave() {\n        this.state.saving = true;\n        try {\n            await this.saveResources();\n        } finally {\n            this.state.saving = false;\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { EditHeadBodyDialog } from \"../edit_head_body_dialog/edit_head_body_dialog\";\nimport { Component, useState } from \"@odoo/owl\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * Represents the warning overlay that appears when the user opens the ResourceEditor\n * It provides options to hide the warning, inject code, and not show the warning again.\n */\nexport class ResourceEditorWarningOverlay extends Component {\n    static template = \"website.ResourceEditorWarningOverlay\";\n    static props = {};\n\n    /**\n     * Initializes the component by setting up the necessary services and state.\n     */\n    setup() {\n        this.website = useService(\"website\");\n        this.dialog = useService(\"dialog\");\n\n        const localStorageValue = browser.localStorage.getItem(\"website.ace.doNotShowWarning\");\n        this.state = useState({\n            visible: !localStorageValue || localStorageValue === \"false\",\n        });\n    }\n\n    /**\n     * Closes the Ace editor and updates the website context to hide it.\n     */\n    onCloseEditor() {\n        this.website.context.showResourceEditor = false;\n    }\n\n    /**\n     * Hides the warning overlay.\n     */\n    onHideWarning() {\n        this.state.visible = false;\n    }\n\n    /**\n     * Sets a flag in the local storage to prevent the warning overlay from\n     * showing again and hides the overlay.\n     */\n    onStopAsking() {\n        browser.localStorage.setItem(\"website.ace.doNotShowWarning\", \"true\");\n        this.onHideWarning();\n    }\n\n    /**\n     * Opens a dialog to edit the head and body of the website and closes the\n     * Ace editor.\n     */\n    onInjectCode() {\n        this.dialog.add(EditHeadBodyDialog);\n        this.onCloseEditor();\n    }\n}\n", "/** @odoo-module */\nimport { _t } from \"@web/core/l10n/translation\";\n\nconst MAPPING = {\n    '{': '}', '}': '{',\n    '(': ')', ')': '(',\n    '[': ']', ']': '[',\n};\nconst OPENINGS = ['{', '(', '['];\nconst CLOSINGS = ['}', ')', ']'];\n\n/**\n * Checks the syntax validity of some SCSS.\n*\n* @param {string} scss\n* @returns {Object} object with keys \"isValid\" and \"error\" if not valid\n*/\nexport function checkSCSS(scss) {\n    const stack = [];\n    let line = 1;\n    for (let i = 0; i < scss.length; i++) {\n        if (OPENINGS.includes(scss[i])) {\n            stack.push(scss[i]);\n        } else if (CLOSINGS.includes(scss[i])) {\n            if (stack.pop() !== MAPPING[scss[i]]) {\n                return {\n                    isValid: false,\n                    error: {\n                        line,\n                        message: _t(\"Unexpected %(char)s\", {char: scss[i]}),\n                    },\n                };\n            }\n        } else if (scss[i] === '\\n') {\n            line++;\n        }\n    }\n    if (stack.length > 0) {\n        return {\n            isValid: false,\n            error: {\n                line,\n                message: _t(\"Expected %(char)s\", {char: MAPPING[stack.pop()]}),\n            },\n        };\n    }\n    return { isValid: true };\n}\n\n/**\n * Checks the syntax validity of some XML.\n *\n * @param {string} xml\n * @returns {Object} object with keys \"isValid\" and \"error\" if not valid\n */\nexport function checkXML(xml) {\n    const xmlDoc = (new window.DOMParser()).parseFromString(xml, 'text/xml');\n    const errorEls = xmlDoc.getElementsByTagName('parsererror');\n    if (errorEls.length > 0) {\n        const errorEl = errorEls[0];\n        const sourceTextEls = errorEl.querySelectorAll('sourcetext');\n        let codeEls = null;\n        if (sourceTextEls.length) {\n            codeEls = [...sourceTextEls].map(el => {\n                const codeEl = document.createElement('code');\n                codeEl.textContent = el.textContent;\n                const brEl = document.createElement('br');\n                brEl.classList.add('o_we_source_text_origin');\n                el.parentElement.insertBefore(brEl, el);\n                return codeEl;\n            });\n            for (const el of sourceTextEls) {\n                el.remove();\n            }\n        }\n        for (const el of [...errorEl.querySelectorAll(':not(code):not(pre):not(br)')]) {\n            const pEl = document.createElement('p');\n            for (const cEl of [...el.childNodes]) {\n                pEl.appendChild(cEl);\n            }\n            el.parentElement.insertBefore(pEl, el);\n            el.remove();\n        }\n        errorEl.querySelectorAll('.o_we_source_text_origin').forEach((el, i) => {\n            el.after(codeEls[i]);\n        });\n        return {    \n            isValid: false,\n            error: {\n                line: parseInt(errorEl.innerHTML.match(/[Ll]ine[^\\d]+(\\d+)/)[1], 10),\n                message: errorEl.textContent,\n            },\n        };\n    }\n    return { isValid: true };\n}\n\n/**\n * Formats some XML so that it has proper indentation and structure.\n *\n * @param {string} xml\n * @returns {string} formatted xml\n */\nexport function formatXML(xml) {\n    // do nothing if an inline script is present to avoid breaking it\n    if (/<script(?: [^>]*)?>[^<][\\s\\S]*<\\/script>/i.test(xml)) {\n        return xml;\n    }\n    return window.vkbeautify.xml(xml, 4);\n}\n", "/** @odoo-module **/\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, onWillStart, useState } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { CodeEditor } from \"@web/core/code_editor/code_editor\";\n\n/**\n * A dialog that let the user edit the code that will be injected in the <head>\n * and before the </body> of every page of the website. This is a stable and\n * upgrade proof alternative to directly editing the website xml.\n */\nexport class EditHeadBodyDialog extends Component {\n    static template = \"website.EditHeadBodyDialog\";\n    static components = { CodeEditor, Dialog };\n    static props = {\n        close: Function,\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.website = useService(\"website\");\n\n        this.state = useState({\n            head: \"\",\n            body: \"\",\n        });\n\n        onWillStart(async () => {\n            const websites = await this.orm.read(\"website\",\n                [this.website.currentWebsite.id],\n                [\"custom_code_head\", \"custom_code_footer\"],\n            );\n            const website = websites[0];\n            this.state.head = website.custom_code_head || \"\";\n            this.state.body = website.custom_code_footer || \"\";\n        });\n    }\n\n    async onSave() {\n        await this.orm.write(\"website\", [this.website.currentWebsite.id], {\n            custom_code_head: this.state.head,\n            custom_code_footer: this.state.body,\n        });\n        this.props.close();\n    }\n}\n", "import { isBrowserFirefox } from \"@web/core/browser/feature_detection\";\nimport { ensureJQuery } from \"@web/core/ensure_jquery\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { useAutofocus, useService } from '@web/core/utils/hooks';\nimport { _t } from \"@web/core/l10n/translation\";\nimport { WebsiteDialog } from '@website/components/dialog/dialog';\nimport { Switch } from '@website/components/switch/switch';\nimport { applyTextHighlight } from \"@website/js/text_processing\";\nimport { useRef, useState, useSubEnv, Component, onWillStart, onMounted } from \"@odoo/owl\";\nimport wUtils from '@website/js/utils';\n\nconst NO_OP = () => {};\n\nexport class AddPageConfirmDialog extends Component {\n    static template = \"website.AddPageConfirmDialog\";\n    static props = {\n        close: Function,\n        onAddPage: {\n            type: Function,\n            optional: true,\n        },\n        websiteId: Number,\n        sectionsArch: {\n            type: String,\n            optional: true,\n        },\n        name: String,\n    };\n    static defaultProps = {\n        onAddPage: NO_OP,\n    };\n    static components = {\n        Switch,\n        WebsiteDialog,\n    };\n\n    setup() {\n        super.setup();\n        useAutofocus();\n\n        this.website = useService('website');\n        this.http = useService('http');\n        this.action = useService('action');\n\n        this.state = useState({\n            addMenu: true,\n            name: this.props.name,\n        });\n    }\n\n    onChangeAddMenu(value) {\n        this.state.addMenu = value;\n    }\n\n    async addPage() {\n        const params = {'add_menu': this.state.addMenu || '', csrf_token: odoo.csrf_token};\n        if (this.props.sectionsArch) {\n            params.sections_arch = this.props.sectionsArch;\n        }\n        // Remove any leading slash.\n        const pageName = this.state.name.replace(/^\\/*/, \"\") || _t(\"New Page\");\n        const url = `/website/add/${encodeURIComponent(pageName)}`;\n        params['website_id'] = this.props.websiteId;\n        const data = await this.http.post(url, params);\n        if (data.view_id) {\n            this.action.doAction({\n                'res_model': 'ir.ui.view',\n                'res_id': data.view_id,\n                'views': [[false, 'form']],\n                'type': 'ir.actions.act_window',\n                'view_mode': 'form',\n            });\n        } else {\n            this.website.goToWebsite({path: data.url, edition: true, websiteId: this.props.websiteId});\n        }\n        this.props.onAddPage(this.state);\n    }\n}\n\nexport class AddPageTemplateBlank extends Component {\n    static template = \"website.AddPageTemplateBlank\";\n    static props = {\n        firstRow: {\n            type: Boolean,\n            optional: true,\n        },\n    };\n\n    setup() {\n        super.setup();\n        this.holderRef = useRef(\"holder\");\n\n        onMounted(async () => {\n            this.holderRef.el.classList.add(\"o_ready\");\n        });\n    }\n\n    select() {\n        this.env.addPage();\n    }\n}\n\nexport class AddPageTemplatePreview extends Component {\n    static template = \"website.AddPageTemplatePreview\";\n    static props = {\n        template: Object,\n        animationDelay: Number,\n        firstRow: {\n            type: Boolean,\n            optional: true,\n        },\n        isCustom: {\n            type: Boolean,\n            optional: true,\n        },\n    };\n\n    setup() {\n        super.setup();\n        this.iframeRef = useRef(\"iframe\");\n        this.previewRef = useRef(\"preview\");\n        this.holderRef = useRef(\"holder\");\n\n        onMounted(async () => {\n            const holderEl = this.holderRef.el;\n            holderEl.classList.add(\"o_loading\");\n            if (!this.props.template.key) {\n                return;\n            }\n            const previewEl = this.previewRef.el;\n            const iframeEl = this.iframeRef.el;\n            // Firefox replaces the built content with about:blank.\n            const isFirefox = isBrowserFirefox();\n            if (isFirefox) {\n                // Make sure empty preview iframe is loaded.\n                // This event is never triggered on Chrome.\n                await new Promise(resolve => {\n                    iframeEl.contentDocument.body.onload = resolve;\n                });\n            }\n            // Apply styles.\n            for (const cssLinkEl of await this.env.getCssLinkEls()) {\n                const preloadLinkEl = document.createElement(\"link\");\n                preloadLinkEl.setAttribute(\"rel\", \"preload\");\n                preloadLinkEl.setAttribute(\"href\", cssLinkEl.getAttribute(\"href\"));\n                preloadLinkEl.setAttribute(\"as\", \"style\");\n                iframeEl.contentDocument.head.appendChild(preloadLinkEl);\n                iframeEl.contentDocument.head.appendChild(cssLinkEl.cloneNode(true));\n            }\n            // Adjust styles.\n            const styleEl = document.createElement(\"style\");\n            // Does not work with fit-content in Firefox.\n            const carouselHeight = isFirefox ? '450px' : 'fit-content';\n            // Prevent successive resizes.\n            const fullHeight = getComputedStyle(document.querySelector(\".o_action_manager\")).height;\n            const halfHeight = `${Math.round(parseInt(fullHeight) / 2)}px`;\n            const css = `\n                html, body {\n                    /* Needed to prevent scrollbar to appear on chrome */\n                    overflow: hidden;\n                }\n                #wrapwrap {\n                    padding-right: 0px;\n                    padding-left: 0px;\n                    --snippet-preview-height: 340px;\n                }\n                section {\n                    /* Avoid the zoom's missing pixel. */\n                    transform: scale(101%);\n                }\n                section[data-snippet=\"s_carousel\"],\n                section[data-snippet=\"s_carousel_intro\"],\n                section[data-snippet=\"s_quotes_carousel_minimal\"],\n                section[data-snippet=\"s_quotes_carousel\"] {\n                    height: ${carouselHeight} !important;\n                }\n                section.o_half_screen_height {\n                    min-height: ${halfHeight} !important;\n                }\n                section.o_full_screen_height {\n                    min-height: ${fullHeight} !important;\n                }\n                section[data-snippet=\"s_three_columns\"] .figure-img[style*=\"height:50vh\"] {\n                    /* In Travel theme. */\n                    height: 170px !important;\n                }\n                .o_we_shape {\n                    /* Avoid the zoom's missing pixel. */\n                    transform: scale(101%);\n                }\n                .o_animate {\n                    visibility: visible;\n                    animation-name: none;\n                }\n            `;\n            const cssText = document.createTextNode(css);\n            styleEl.appendChild(cssText);\n            iframeEl.contentDocument.head.appendChild(styleEl);\n            // Put blocks.\n            // To preserve styles, the whole #wrapwrap > main > #wrap\n            // nesting must be reproduced.\n            const mainEl = document.createElement(\"main\");\n            const wrapwrapEl = document.createElement(\"div\");\n            wrapwrapEl.id = \"wrapwrap\";\n            wrapwrapEl.appendChild(mainEl);\n            iframeEl.contentDocument.body.appendChild(wrapwrapEl);\n            const templateDocument = new DOMParser().parseFromString(this.props.template.template, \"text/html\");\n            const wrapEl = templateDocument.getElementById(\"wrap\");\n            mainEl.appendChild(wrapEl);\n            // Make image loading eager.\n            const lazyLoadedImgEls = wrapEl.querySelectorAll(\"img[loading=lazy]\");\n            for (const imgEl of lazyLoadedImgEls) {\n                imgEl.setAttribute(\"loading\", \"eager\");\n            }\n            mainEl.appendChild(wrapEl);\n            await ensureJQuery();\n            await wUtils.onceAllImagesLoaded($(wrapEl));\n            // Restore image lazy loading.\n            for (const imgEl of lazyLoadedImgEls) {\n                imgEl.setAttribute(\"loading\", \"lazy\");\n            }\n            if (!this.previewRef.el) {\n                // Stop the process when preview is removed\n                return;\n            }\n            // Wait for fonts.\n            await iframeEl.contentDocument.fonts.ready;\n            holderEl.classList.remove(\"o_loading\");\n            const adjustHeight = () => {\n                if (!this.previewRef.el) {\n                    // Stop ajusting height when preview is removed.\n                    return;\n                }\n                const outerWidth = parseInt(window.getComputedStyle(previewEl).width);\n                const innerHeight = wrapEl.getBoundingClientRect().height;\n                const innerWidth = wrapEl.getBoundingClientRect().width;\n                const ratio = outerWidth / innerWidth;\n                iframeEl.height = Math.round(innerHeight);\n                previewEl.style.setProperty(\"height\", `${Math.round(innerHeight * ratio)}px`);\n                // Sometimes the final height is not ready yet.\n                setTimeout(adjustHeight, 50);\n                holderEl.classList.add(\"o_ready\");\n            };\n            adjustHeight();\n            if (this.props.isCustom) {\n                this.adaptCustomTemplate(wrapEl);\n            }\n            // We need this to correctly compute the highlights size (the\n            // `ResizeObserver` that adapts the effects when a custom font\n            // is applied is not available), for now, we need a setTimeout.\n            setTimeout(() => {\n                for (const textEl of iframeEl.contentDocument?.querySelectorAll(\".o_text_highlight\") || []) {\n                    applyTextHighlight(textEl);\n                }\n            }, 200);\n        });\n    }\n\n    adaptCustomTemplate(wrapEl) {\n        for (const sectionEl of wrapEl.querySelectorAll(\"section:not(.o_snippet_desktop_invisible)\")) {\n            const style = window.getComputedStyle(sectionEl);\n            if (!style.height || style.display === 'none') {\n                const messageEl = renderToElement(\"website.AddPageTemplatePreviewDynamicMessage\", {\n                    message: _t(\n                        \"No preview for the %s block because it is dynamically rendered.\",\n                        sectionEl.dataset.name\n                    ),\n                });\n                sectionEl.insertAdjacentElement(\"beforebegin\", messageEl);\n            }\n        }\n    }\n\n    select() {\n        if (this.holderRef.el.classList.contains(\"o_loading\")) {\n            return;\n        }\n        const wrapEl = this.iframeRef.el.contentDocument.getElementById(\"wrap\").cloneNode(true);\n        for (const previewEl of wrapEl.querySelectorAll(\".o_new_page_snippet_preview, .s_dialog_preview\")) {\n            previewEl.remove();\n        }\n        this.env.addPage(wrapEl.innerHTML, this.props.template.name && _t(\"Copy of %s\", this.props.template.name));\n    }\n}\n\nexport class AddPageTemplatePreviews extends Component {\n    static template = \"website.AddPageTemplatePreviews\";\n    static props = {\n        isCustom: {\n            type: Boolean,\n            optional: true,\n        },\n        templates: {\n            type: Array,\n            element: Object,\n        },\n    };\n    static components = {\n        AddPageTemplateBlank,\n        AddPageTemplatePreview,\n    };\n\n    setup() {\n        super.setup();\n    }\n\n    get columns() {\n        const result = [[], [], []];\n        let currentColumnIndex = 0;\n        for (const template of this.props.templates) {\n            result[currentColumnIndex].push(template);\n            currentColumnIndex = (currentColumnIndex + 1) % result.length;\n        }\n        return result;\n    }\n}\n\nexport class AddPageTemplates extends Component {\n    static template = \"website.AddPageTemplates\";\n    static props = {\n        onTemplatePageChanged: Function,\n    };\n    static components = {\n        AddPageTemplatePreviews,\n    };\n\n    setup() {\n        super.setup();\n        this.tabsRef = useRef(\"tabs\");\n        this.panesRef = useRef(\"panes\");\n\n        this.state = useState({\n            pages: [{\n                Component: AddPageTemplatePreviews,\n                title: _t(\"Loading...\"),\n                isPreloading: true,\n                props: {\n                    id: \"basic\",\n                    title: _t(\"Basic\"),\n                    // Blank and 5 preloading boxes.\n                    templates: [{ isBlank: true }, {}, {}, {}, {}, {}],\n                },\n            }],\n        });\n        this.pages = undefined;\n\n        onWillStart(() => {\n            this.preparePages().then(pages => {\n                this.state.pages = pages;\n            });\n        });\n    }\n\n    async preparePages() {\n        // Forces the correct website if needed before fetching the templates.\n        // Displaying the correct images in the previews also relies on the\n        // website id having been forced.\n        await this.env.getCssLinkEls();\n\n        if (this.pages) {\n            return this.pages;\n        }\n\n        const newPageTemplates = await rpc(\"/website/get_new_page_templates\");\n        newPageTemplates[0].templates.unshift({\n            isBlank: true,\n        });\n        const pages = [];\n        for (const template of newPageTemplates) {\n            pages.push({\n                Component: AddPageTemplatePreviews,\n                title: template.title,\n                props: template,\n                id: `${template.id}`,\n            });\n        }\n        this.pages = pages;\n        return pages;\n    }\n\n    onTabClick(id) {\n        for (const page of this.state.pages) {\n            if (page.id === id) {\n                page.isAccessed = true;\n            }\n        }\n        const activeTabEl = this.tabsRef.el.querySelector(\".active\");\n        const activePaneEl = this.panesRef.el.querySelector(\".active\");\n        activeTabEl?.classList?.remove(\"active\");\n        activePaneEl?.classList?.remove(\"active\");\n        const tabEl = this.tabsRef.el.querySelector(`[data-id=${id}]`);\n        const paneEl = this.panesRef.el.querySelector(`[data-id=${id}]`);\n        tabEl.classList.add(\"active\");\n        paneEl.classList.add(\"active\");\n        this.props.onTemplatePageChanged(tabEl.dataset.id === \"basic\" ? \"\" : tabEl.textContent);\n    }\n}\n\nexport class AddPageDialog extends Component {\n    static template = \"website.AddPageDialog\";\n    static props = {\n        close: Function,\n        onAddPage: {\n            type: Function,\n            optional: true,\n        },\n        websiteId: {\n            type: Number,\n        },\n    };\n    static defaultProps = {\n        onAddPage: NO_OP,\n    };\n    static components = {\n        WebsiteDialog,\n        AddPageTemplates,\n        AddPageTemplatePreviews,\n    };\n\n    setup() {\n        super.setup();\n        useAutofocus();\n\n        this.primaryTitle = _t(\"Create\");\n        this.switchLabel = _t(\"Add to menu\");\n        this.website = useService('website');\n        this.dialogs = useService(\"dialog\");\n        this.orm = useService('orm');\n        this.http = useService('http');\n        this.action = useService('action');\n\n        this.cssLinkEls = undefined;\n        this.lastTabName = \"\";\n\n        useSubEnv({\n            addPage: (sectionsArch, name) => this.addPage(sectionsArch, name),\n            getCssLinkEls: () => this.getCssLinkEls(),\n        });\n    }\n\n    onTemplatePageChanged(name) {\n        this.lastTabName = name;\n    }\n\n    async addPage(sectionsArch, name) {\n        const props = this.props;\n        this.dialogs.add(AddPageConfirmDialog, {\n            onAddPage: () => {\n                props.onAddPage();\n                props.close();\n            },\n            websiteId: this.props.websiteId,\n            sectionsArch: sectionsArch,\n            name: name || this.lastTabName,\n        });\n    }\n\n    getCssLinkEls() {\n        if (!this.cssLinkEls) {\n            this.cssLinkEls = new Deferred();\n            (async () => {\n                let contentDocument;\n                // Already in DOM ?\n                const pageIframeEl = document.querySelector(\"iframe.o_iframe\");\n                if (pageIframeEl?.getAttribute(\"is-ready\") === \"true\") {\n                    // If there is a fully loaded website preview, use it.\n                    contentDocument = pageIframeEl.contentDocument;\n                }\n                if (!contentDocument) {\n                    // If there is no website preview or it was not ready yet, fetch page.\n                    const html = await this.http.get(`/website/force/${this.props.websiteId}?path=/`, \"text\");\n                    contentDocument = new DOMParser().parseFromString(html, \"text/html\");\n                }\n                this.cssLinkEls.resolve(contentDocument.head.querySelectorAll(\"link[type='text/css']\"));\n            })();\n        }\n        return this.cssLinkEls;\n    }\n}\n", "/** @odoo-module **/\n\nimport { Dialog } from '@web/core/dialog/dialog';\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useState, Component } from \"@odoo/owl\";\n\nconst NO_OP = () => {};\n\nexport class WebsiteDialog extends Component {\n    static template = \"website.WebsiteDialog\";\n    static components = { Dialog };\n    static props = {\n        ...Dialog.props,\n        primaryTitle: { type: String, optional: true },\n        primaryClick: { type: Function, optional: true },\n        secondaryTitle: { type: String, optional: true },\n        secondaryClick: { type: Function, optional: true },\n        showSecondaryButton: { type: Boolean, optional: true },\n        close: { type: Function, optional: true },\n        closeOnClick: { type: Boolean, optional: true },\n        body: { type: String, optional: true },\n        slots: { type: Object, optional: true },\n        showFooter: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        ...Dialog.defaultProps,\n        title: _t(\"Confirmation\"),\n        showFooter: true,\n        primaryTitle: _t(\"Ok\"),\n        secondaryTitle: _t(\"Cancel\"),\n        showSecondaryButton: true,\n        size: \"md\",\n        closeOnClick: true,\n        close: NO_OP,\n    };\n\n    setup() {\n        this.state = useState({\n            disabled: false,\n        });\n    }\n    /**\n     * Disables the buttons of the dialog when a click is made.\n     * If a handler is provided, await for its call.\n     * If the prop closeOnClick is true, close the dialog.\n     * Otherwise, restore the button.\n     *\n     * @param handler {function|void} The handler to protect.\n     * @returns {function(): Promise} handler called when a click is made.\n     */\n    protectedClick(handler) {\n        return async () => {\n            if (this.state.disabled) {\n                return;\n            }\n            this.state.disabled = true;\n            if (handler) {\n                await handler();\n            }\n            if (this.props.closeOnClick) {\n                return this.props.close();\n            }\n            this.state.disabled = false;\n        }\n    }\n\n    get contentClasses() {\n        const websiteDialogClass = 'o_website_dialog';\n        if (this.props.contentClass) {\n            return `${websiteDialogClass} ${this.props.contentClass}`;\n        }\n        return websiteDialogClass;\n    }\n}\n", "import { useService, useAutofocus } from '@web/core/utils/hooks';\nimport { useNestedSortable } from \"@web/core/utils/nested_sortable\";\nimport wUtils from '@website/js/utils';\nimport { WebsiteDialog } from './dialog';\nimport { Component, useState, useEffect, onWillStart, useRef } from \"@odoo/owl\";\n\nconst useControlledInput = (initialValue, validate) => {\n    const input = useState({\n        value: initialValue,\n        hasError: false,\n    });\n\n    const isValid = () => {\n        if (validate(input.value)) {\n            return true;\n        }\n        input.hasError = true;\n        return false;\n    };\n\n    useEffect(() => {\n        input.hasError = false;\n    }, () => [input.value]);\n\n    return {\n        input,\n        isValid,\n    };\n};\n\nexport class MenuDialog extends Component {\n    static template = \"website.MenuDialog\";\n    static components = { WebsiteDialog };\n    static props = {\n        name: { type: String, optional: true },\n        url: { type: String, optional: true },\n        isMegaMenu: { type: Boolean, optional: true },\n        save: Function,\n        close: Function,\n    };\n\n    setup() {\n        this.website = useService('website');\n        useAutofocus();\n\n        this.name = useControlledInput(this.props.name, value => !!value);\n        this.url = useControlledInput(this.props.url, value => !!value);\n        this.urlInputRef = useRef('url-input');\n\n        useEffect((input) => {\n            if (!input) {\n                return;\n            }\n            const options = {\n                body: this.website.pageDocument.body,\n                position: \"bottom-fit\",\n                classes: {\n                    'ui-autocomplete': 'o_edit_menu_autocomplete'\n                },\n                urlChosen: () => {\n                    this.url.input.value = input.value;\n                },\n            };\n            const unmountAutocompleteWithPages = wUtils.autocompleteWithPages(input, options);\n            return () => unmountAutocompleteWithPages();\n        }, () => [this.urlInputRef.el]);\n    }\n\n    onClickOk() {\n        if (this.name.isValid()) {\n            if (this.props.isMegaMenu || this.url.isValid()) {\n                this.props.save(this.name.input.value, this.url.input.value);\n                this.props.close();\n            }\n        }\n    }\n}\n\nclass MenuRow extends Component {\n    static template = \"website.MenuRow\";\n    static props = {\n        menu: Object,\n        edit: Function,\n        delete: Function,\n    };\n    static components = {\n        MenuRow,\n    };\n\n    edit() {\n        this.props.edit(this.props.menu.fields['id']);\n    }\n\n    delete() {\n        this.props.delete(this.props.menu.fields['id']);\n    }\n}\n\nexport class EditMenuDialog extends Component {\n    static template = \"website.EditMenuDialog\";\n    static components = {\n        MenuRow,\n        WebsiteDialog,\n    };\n    static props = [\"rootID?\", \"close\", \"save?\"];\n\n    setup() {\n        this.orm = useService('orm');\n        this.website = useService('website');\n        this.dialogs = useService('dialog');\n\n        this.menuEditor = useRef('menu-editor');\n\n        this.state = useState({ rootMenu: {} });\n\n        onWillStart(async () => {\n            const menu = await this.orm.call(\n                'website.menu',\n                'get_tree',\n                [this.website.currentWebsite.id, this.props.rootID],\n                { context: { lang: this.website.currentWebsite.metadata.lang } }\n            );\n            this.state.rootMenu = menu;\n            this.map = new Map();\n            this.populate(this.map, this.state.rootMenu);\n            this.toDelete = [];\n        });\n\n        useNestedSortable({\n            ref: this.menuEditor,\n            handle: \"div\",\n            nest: true,\n            maxLevels: 2,\n            onDrop: this._moveMenu.bind(this),\n            isAllowed: this._isAllowedMove.bind(this),\n            useElementSize: true,\n        });\n    }\n\n    populate(map, menu) {\n        map.set(menu.fields['id'], menu);\n        for (const submenu of menu.children) {\n            this.populate(map, submenu);\n        }\n    }\n\n    _isAllowedMove(current, elementSelector) {\n        const currentIsMegaMenu = current.element.dataset.isMegaMenu === \"true\";\n        if (!currentIsMegaMenu) {\n            return current.placeHolder.parentNode.closest(`${elementSelector}[data-is-mega-menu=\"true\"]`) === null;\n        }\n        const isDropOnRoot = current.placeHolder.parentNode.closest(elementSelector) === null;\n        return currentIsMegaMenu && isDropOnRoot;\n    }\n\n    _getMenuIdForElement(element) {\n        const menuIdStr = element.dataset.menuId;\n        const menuId = parseInt(menuIdStr);\n        return isNaN(menuId) ? menuIdStr : menuId;\n    }\n\n    _moveMenu({ element, parent, previous }) {\n        const menuId = this._getMenuIdForElement(element);\n        const menu = this.map.get(menuId);\n\n        // Remove element from parent's children (since we are moving it, this is the mandatory first step)\n        const parentId = menu.fields['parent_id'] || this.state.rootMenu.fields['id'];\n        let parentMenu = this.map.get(parentId);\n        parentMenu.children = parentMenu.children.filter((m) => m.fields['id'] !== menuId);\n\n        // Determine next parent\n        const menuParentId = parent ? this._getMenuIdForElement(parent.closest(\"li\")) : this.state.rootMenu.fields['id'];\n        parentMenu = this.map.get(menuParentId);\n        menu.fields['parent_id'] = parentMenu.fields['id'];\n\n        // Determine at which position we should place the element\n        if (previous) {\n            const previousMenu = this.map.get(this._getMenuIdForElement(previous));\n            const index = parentMenu.children.findIndex((menu) => menu === previousMenu);\n            parentMenu.children.splice(index + 1, 0, menu);\n        } else {\n            parentMenu.children.unshift(menu);\n        }\n    }\n\n    addMenu(isMegaMenu) {\n        this.dialogs.add(MenuDialog, {\n            isMegaMenu,\n            save: (name, url, isNewWindow) => {\n                const newMenu = {\n                    fields: {\n                        id: `menu_${(new Date).toISOString()}`,\n                        name,\n                        url: isMegaMenu ? '#' : url,\n                        new_window: isNewWindow,\n                        'is_mega_menu': isMegaMenu,\n                        sequence: 0,\n                        'parent_id': false,\n                    },\n                    'children': [],\n                };\n                this.state.rootMenu.children.push(newMenu);\n                // this.state.rootMenu.children.at(-1) to forces a rerender\n                this.map.set(newMenu.fields[\"id\"], this.state.rootMenu.children.at(-1));\n            },\n        });\n    }\n\n    editMenu(id) {\n        const menuToEdit = this.map.get(id);\n        this.dialogs.add(MenuDialog, {\n            name: menuToEdit.fields['name'],\n            url: menuToEdit.fields['url'],\n            isMegaMenu: menuToEdit.fields['is_mega_menu'],\n            save: (name, url) => {\n                menuToEdit.fields['name'] = name;\n                menuToEdit.fields['url'] = url;\n            },\n        });\n    }\n\n    deleteMenu(id) {\n        const menuToDelete = this.map.get(id);\n\n        // Delete children first\n        for (const child of menuToDelete.children) {\n            this.deleteMenu(child.fields.id);\n        }\n\n        const parentId = menuToDelete.fields['parent_id'] || this.state.rootMenu.fields['id'];\n        const parent = this.map.get(parentId);\n        parent.children = parent.children.filter(menu => menu.fields['id'] !== id);\n        this.map.delete(id);\n        if (parseInt(id)) {\n            this.toDelete.push(id);\n        }\n    }\n\n    async onClickSave() {\n        const data = [];\n        this.map.forEach((menu, id) => {\n            if (this.state.rootMenu.fields['id'] !== id) {\n                const menuFields = menu.fields;\n                const parentId = menuFields.parent_id || this.state.rootMenu.fields['id'];\n                const parentMenu = this.map.get(parentId);\n                menuFields['sequence'] = parentMenu.children.findIndex(m => m.fields['id'] === id);\n                menuFields['parent_id'] = parentId;\n                data.push(menuFields);\n            }\n        });\n\n        await this.orm.call('website.menu', 'save', [\n            this.website.currentWebsite.id,\n            {\n                'data': data,\n                'to_delete': this.toDelete,\n            }\n        ],\n        { context: { lang: this.website.currentWebsite.metadata.lang } });\n        if (this.props.save) {\n            this.props.save();\n        } else {\n            this.website.goToWebsite();\n        }\n    }\n}\n", "import {CheckBox} from '@web/core/checkbox/checkbox';\nimport { _t } from \"@web/core/l10n/translation\";\nimport {useService, useAutofocus} from \"@web/core/utils/hooks\";\nimport {sprintf} from \"@web/core/utils/strings\";\nimport {WebsiteDialog} from './dialog';\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport {FormViewDialog} from \"@web/views/view_dialogs/form_view_dialog\";\nimport { formView } from '@web/views/form/form_view';\nimport { renderToFragment } from \"@web/core/utils/render\";\nimport { Component, onWillDestroy, useEffect, useRef, useState, xml } from \"@odoo/owl\";\nimport { FormController } from '@web/views/form/form_controller';\nimport { registry } from \"@web/core/registry\";\n\nexport class PageDependencies extends Component {\n    static template = \"website.PageDependencies\";\n    static popoverTemplate = xml`\n        <div class=\"popover o_page_dependencies\" role=\"tooltip\">\n            <div class=\"arrow\"/>\n            <h3 class=\"popover-header\"/>\n            <div class=\"popover-body\"/>\n        </div>\n    `;\n    static props = {\n        resIds: Array,\n        resModel: String,\n        mode: String,\n    };\n\n    setup() {\n        super.setup();\n        this.orm = useService('orm');\n\n        this.action = useRef('action');\n        this.sprintf = sprintf;\n\n        useEffect(\n            () => {\n                this.fetchDependencies();\n            },\n            () => []\n        );\n        this.state = useState({\n            dependencies: {},\n        });\n\n        onWillDestroy(async () => {\n            await this.destroyDependenciesPopover();\n        });\n    }\n\n    async getResIds() {\n        return this.props.resIds;\n    }\n\n    async fetchDependencies() {\n        this.state.dependencies = await this.orm.call(\n            'website',\n            'search_url_dependencies',\n            [this.props.resModel, await this.getResIds()],\n        );\n    }\n\n    showDependencies() {\n        const popover = window.Popover.getOrCreateInstance(this.action.el, {\n            title: _t(\"Dependencies\"),\n            boundary: 'viewport',\n            placement: 'right',\n            trigger: 'focus',\n            content: () => {\n                return renderToFragment(\"website.PageDependencies.Tooltip\", {\n                    dependencies: this.state.dependencies,\n                });\n            },\n        });\n        popover.toggle();\n    }\n\n    async destroyDependenciesPopover() {\n        const actionEl = this.action.el;\n        const popover = window.Popover.getInstance(actionEl);\n        if (popover) {\n            // If popover is hiding (animation), wait for the animation to\n            // complete.\n            if (!popover.tip.classList.contains(\"show\")) {\n                await new Promise((resolve) => {\n                    const handler = () => {\n                        actionEl.removeEventListener(\"hidden.bs.popover\", handler);\n                        resolve();\n                    };\n                    actionEl.addEventListener(\"hidden.bs.popover\", handler);\n                });\n            }\n            popover.dispose();\n        }\n    }\n}\n\nexport class FormPageDependencies extends PageDependencies {\n    static props = {\n        ...standardFieldProps,\n        ...PageDependencies.props,\n        resIds: { type: Array, optional: true },\n    };\n\n    async getResIds() {\n        const records = await this.orm.read(\n            this.props.record.resModel,\n            [this.props.record.resId],\n            [\"target_model_id\"],\n        );\n        return records.map((record) => record.target_model_id[0]);\n    }\n}\n\nexport const formPageDependenciesWidget = {\n    component: FormPageDependencies,\n    extractProps: ({ attrs }) => {\n        const { mode, name, resModel, resIds } = attrs;\n        return {\n            mode,\n            name: name || \"\",\n            resModel,\n            resIds,\n        };\n    },\n};\nregistry.category(\"view_widgets\").add(\"form_page_dependencies\", formPageDependenciesWidget);\n\nexport class DeletePageDialog extends Component {\n    static template = \"website.DeletePageDialog\";\n    static components = {\n        PageDependencies,\n        CheckBox,\n        WebsiteDialog,\n    };\n    static props = {\n        resIds: Array,\n        resModel: String,\n        onDelete: { type: Function, optional: true },\n        close: Function,\n        hasNewPageTemplate: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        this.website = useService('website');\n\n        this.state = useState({\n            confirm: false,\n        });\n    }\n\n    onConfirmCheckboxChange(checked) {\n        this.state.confirm = checked;\n    }\n\n    onClickDelete() {\n        this.props.close();\n        this.props.onDelete();\n    }\n}\n\nexport class DuplicatePageDialog extends Component {\n    static components = { WebsiteDialog };\n    static template = \"website.DuplicatePageDialog\";\n    static props = {\n        onDuplicate: Function,\n        close: Function,\n        pageIds: { type: Array, element: Number },\n    };\n\n    setup() {\n        this.orm = useService('orm');\n        this.website = useService('website');\n        useAutofocus();\n\n        this.state = useState({\n            name: '',\n        });\n    }\n\n    async duplicate() {\n        const duplicates = [];\n        if (this.state.name) {\n            for (let count = 0; count < this.props.pageIds.length; count++) {\n                const name = this.state.name + (count ? ` ${count + 1}` : \"\");\n                duplicates.push(await this.orm.call(\n                    'website.page',\n                    'clone_page',\n                    [this.props.pageIds[count], name]\n                ));\n            }\n        }\n        this.props.onDuplicate(duplicates);\n    }\n}\n\nexport class PagePropertiesFormController extends FormController {\n    static props = {\n        ...FormController.props,\n        clonePage: { type: Function, optional: true },\n        deletePage: { type: Function, optional: true },\n    };\n}\n\nregistry.category(\"views\").add(\"page_properties_dialog_form\", {\n    ...formView,\n    Controller: PagePropertiesFormController,\n});\n\nexport class PagePropertiesDialog extends FormViewDialog {\n    static props = {\n        ...FormViewDialog.props,\n        onClose: { type: Function, optional: true },\n        resModel: { type: String, optional: true },\n    };\n\n    static defaultProps = {\n        ...FormViewDialog.defaultProps,\n        title: _t(\"Page Properties\"),\n        size: \"md\",\n        onClose: () => {},\n    };\n\n    setup() {\n        super.setup();\n        this.dialog = useService('dialog');\n        this.orm = useService('orm');\n        this.website = useService('website');\n\n        this.viewProps = {\n            ...this.viewProps,\n            resId: this.resId,\n            resModel: this.resModel,\n            context: Object.assign(\n                {\n                    form_view_ref: this.isPage\n                        ? \"website.website_page_properties_view_form\"\n                        : \"website.website_page_properties_base_view_form\",\n                },\n                this.viewProps.context,\n            ),\n            ...(this.isPage\n                ? {\n                      buttonTemplate: \"website.PagePropertiesDialogButtons\",\n                      clonePage: this.clonePage.bind(this),\n                      deletePage: this.deletePage.bind(this),\n                  }\n                : {}),\n        };\n    }\n\n    get resId() {\n        return this.props.resId;\n    }\n\n    get resModel() {\n        if (this.props.resModel) {\n            return this.props.resModel;\n        }\n        return this.isPage ? \"website.page.properties\" : \"website.page.properties.base\";\n    }\n\n    get targetId() {\n        return this.website.currentWebsite?.metadata.mainObject.id;\n    }\n\n    get targetModel() {\n        return this.website.currentWebsite?.metadata.mainObject.model;\n    }\n\n    get isPage() {\n        return this.targetModel === \"website.page\";\n    }\n\n    clonePage() {\n        this.dialog.add(DuplicatePageDialog, {\n            pageIds: [this.targetId],\n            onDuplicate: (duplicates) => {\n                this.props.close();\n                this.props.onClose();\n                this.website.goToWebsite({ path: duplicates[0], edition: true });\n            },\n        });\n    }\n\n    async deletePage() {\n        const pageIds = [this.targetId];\n        const newPageTemplateFields = await this.orm.read(\"website.page\", pageIds, [\"is_new_page_template\"]);\n        this.dialog.add(DeletePageDialog, {\n            resIds: pageIds,\n            resModel: 'website.page',\n            onDelete: async () => {\n                await this.orm.unlink(\"website.page\", pageIds);\n                this.website.goToWebsite({path: '/'});\n                this.props.close();\n                this.props.onClose();\n            },\n            hasNewPageTemplate: newPageTemplateFields[0].is_new_page_template,\n        });\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { pyToJsLocale, jsToPyLocale } from \"@web/core/l10n/utils\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { escapeRegExp } from \"@web/core/utils/strings\";\nimport { useService, useAutofocus } from '@web/core/utils/hooks';\nimport { MediaDialog } from '@web_editor/components/media_dialog/media_dialog';\nimport { WebsiteDialog } from './dialog';\nimport { Component, useState, reactive, onMounted, onWillStart, useEffect } from \"@odoo/owl\";\n\n// This replaces \\b, because accents(e.g. \u00e0, \u00e9) are not seen as word boundaries.\n// Javascript \\b is not unicode aware, and words beginning or ending by accents won't match \\b\nconst WORD_SEPARATORS_REGEX = '([\\\\u2000-\\\\u206F\\\\u2E00-\\\\u2E7F\\'!\"#\\\\$%&\\\\(\\\\)\\\\*\\\\+,\\\\-\\\\.\\\\/:;<=>\\\\?\u00bf\u00a1@\\\\[\\\\]\\\\^_`\\\\{\\\\|\\\\}~\\\\s]+|^|$)';\n\nconst seoContext = reactive({\n    description: '',\n    keywords: [],\n    title: '',\n    seoName: '',\n    metaImage: '',\n    defaultTitle: '',\n});\n\nclass MetaImage extends Component {\n    static template = \"website.MetaImage\";\n    static props = [\"active\", \"src\", \"custom\", \"selectImage\"];\n}\n\nclass ImageSelector extends Component {\n    static template = \"website.ImageSelector\";\n    static components = {\n        MetaImage,\n    };\n    static props = {\n        previewDescription: String,\n        defaultTitle: String,\n        hasSocialDefaultImage: Boolean,\n        pageImages: Array,\n        url: String,\n    };\n\n    setup() {\n        this.website = useService('website');\n        this.dialogs = useService('dialog');\n\n        this.seoContext = useState(seoContext);\n\n        const firstImageId = this.props.hasSocialDefaultImage ? 'social_default_image' : 'logo';\n        const firstImageSrc = `/web/image/website/${encodeURIComponent(this.website.currentWebsite.id)}/${firstImageId}`;\n        const firstImage = {\n            src: firstImageSrc,\n            active: this.areSameImages(firstImageSrc, this.seoContext.metaImage),\n            custom: false,\n        };\n\n        this.state = useState({\n            images: [\n                firstImage,\n                ...this.props.pageImages.map((src) => {\n                    return {\n                        src,\n                        active: this.areSameImages(src, this.seoContext.metaImage),\n                        custom: false,\n                    };\n                }),\n            ],\n        });\n\n        if (this.seoContext.metaImage && !this.state.images.map(({src}) => this.getImagePathname(src)).includes(this.getImagePathname(this.seoContext.metaImage))) {\n            this.state.images.push({\n                src: this.seoContext.metaImage,\n                active: true,\n                custom: true,\n            });\n        }\n\n        if (!this.activeMetaImage) {\n            this.selectImage(this.state.images[0].src);\n        }\n    }\n\n    get title() {\n        return this.seoContext.title || this.props.defaultTitle;\n    }\n\n    get description() {\n        return this.seoContext.description || this.props.previewDescription;\n    }\n\n    get activeMetaImage() {\n        const activeImage = this.state.images.find(({active}) => active);\n        return activeImage && activeImage.src;\n    }\n\n    getImagePathname(src) {\n        return new URL(src, this.website.pageDocument.location.origin).pathname;\n    }\n\n    areSameImages(src1, src2) {\n        return this.getImagePathname(src1) === this.getImagePathname(src2);\n    }\n\n    selectImage(src) {\n        this.state.images = this.state.images.map(img => {\n            img.active = img.src === src;\n            return img;\n        });\n        this.seoContext.metaImage = src;\n    }\n\n    openMediaDialog() {\n        this.dialogs.add(MediaDialog, {\n            // onlyImages: true,\n            resModel: 'ir.ui.view',\n            useMediaLibrary: true,\n            save: image => {\n                let existingImage;\n                this.state.images = this.state.images.map(img => {\n                    img.active = false;\n                    if (img.src === image.src) {\n                        existingImage = img;\n                        img.active = true;\n                    }\n                    return img;\n                });\n                if (!existingImage) {\n                    this.state.images.push({\n                        src: image.src,\n                        active: true,\n                        custom: true,\n                    });\n                }\n                this.seoContext.metaImage = image.src;\n            },\n        });\n    }\n}\n\nclass Keyword extends Component {\n    static template = \"website.Keyword\";\n    static props = {\n        language: String,\n        keyword: String,\n        addKeyword: Function,\n        removeKeyword: Function,\n    };\n\n    setup() {\n        this.website = useService('website');\n\n        this.seoContext = useState(seoContext);\n\n        this.state = useState({\n            suggestions: [],\n        });\n\n        onMounted(async () => {\n            const suggestions = await rpc('/website/seo_suggest', {\n                lang: jsToPyLocale(this.props.language),\n                keywords: this.props.keyword,\n            });\n            const regex = new RegExp(\n                WORD_SEPARATORS_REGEX + escapeRegExp(this.props.keyword) + WORD_SEPARATORS_REGEX,\n                \"gi\"\n            );\n            this.state.suggestions = [...new Set(JSON.parse(suggestions).map(word => word.replace(regex, '').trim()))];\n        });\n    }\n\n    isKeywordIn(string) {\n        return new RegExp(\n            WORD_SEPARATORS_REGEX + escapeRegExp(this.props.keyword) + WORD_SEPARATORS_REGEX,\n            \"gi\"\n        ).test(string);\n    }\n\n    getHeaders(tag) {\n        return Array.from(this.website.pageDocument.documentElement.querySelectorAll(`#wrap ${tag}`)).map(header => header.textContent);\n    }\n\n    getBodyText() {\n        return this.website.pageDocument.body.textContent;\n    }\n\n    get usedInH1() {\n        return this.isKeywordIn(this.getHeaders('h1'));\n    }\n\n    get usedInH2() {\n        return this.isKeywordIn(this.getHeaders('h2'));\n    }\n\n    get usedInTitle() {\n        return this.isKeywordIn(this.seoContext.title || this.seoContext.defaultTitle);\n    }\n\n    get usedInDescription() {\n        return this.isKeywordIn(this.seoContext.description);\n    }\n\n    get usedInContent() {\n        return this.isKeywordIn(this.getBodyText());\n    }\n}\n\nclass MetaKeywords extends Component {\n    static template = \"website.MetaKeywords\";\n    static components = {\n        Keyword,\n    };\n    static props = {};\n\n    setup() {\n        this.website = useService('website');\n\n        this.seoContext = useState(seoContext);\n\n        this.state = useState({\n            language: '',\n            keyword: '',\n        });\n\n        this.maxKeywords = 10;\n\n        onWillStart(async () => {\n            this.languages = await rpc('/website/get_languages');\n            this.state.language = this.getLanguage();\n        });\n    }\n\n    onKeyup(ev) {\n        // Add keyword on enter.\n        if (ev.key === \"Enter\") {\n            this.addKeyword(this.state.keyword);\n        }\n    }\n\n    getLanguage() {\n        return (\n            pyToJsLocale(this.website.pageDocument.documentElement.getAttribute(\"lang\")) || \"en-US\"\n        );\n    }\n\n    get isFull() {\n        return this.seoContext.keywords.length >= this.maxKeywords;\n    }\n\n    addKeyword(keyword) {\n        keyword = keyword.replaceAll(/,\\s*/gi, \" \").trim();\n        if (keyword && !this.isFull && !this.seoContext.keywords.includes(keyword)) {\n            this.seoContext.keywords.push(keyword);\n            this.state.keyword = '';\n        }\n    }\n\n    removeKeyword(keyword) {\n        this.seoContext.keywords = this.seoContext.keywords.filter(kw => kw !== keyword);\n    }\n}\n\nclass SEOPreview extends Component {\n    static template = \"website.SEOPreview\";\n    static props = {\n        isIndexed: Boolean,\n        title: String,\n        description: String,\n        url: String,\n    };\n\n    get description() {\n        if (this.props.description.length > 160) {\n            return this.props.description.substring(0, 159) + '\u2026';\n        }\n        return this.props.description;\n    }\n}\nclass TitleDescription extends Component {\n    static template = \"website.TitleDescription\";\n    static props = {\n        canEditDescription: Boolean,\n        canEditUrl: Boolean,\n        canEditTitle: Boolean,\n        seoNameHelp: String,\n        seoNameDefault: { optional: true, String },\n        isIndexed: Boolean,\n        defaultTitle: String,\n        previewDescription: String,\n        url: String,\n    };\n    static components = {\n        SEOPreview,\n    };\n\n    setup() {\n        this.seoContext = useState(seoContext);\n        useAutofocus();\n\n        this.previousSeoName = this.seoContext.seoName;\n\n        this.maxRecommendedDescriptionSize = 300;\n        this.minRecommendedDescriptionSize = 50;\n\n        // Update the title when its input value changes\n        useEffect(() => {\n            document.title = this.title;\n        }, () => [this.seoContext.title]);\n\n        // Restore the original title when unmounting the component\n        useEffect(() => {\n            const initialTitle = document.title;\n            return () => document.title = initialTitle;\n        }, () => []);\n    }\n\n    //--------------------------------------------------------------------------\n    // Getters\n    //--------------------------------------------------------------------------\n\n    get seoNameUrl() {\n        return this.previousSeoName || this.props.seoNameDefault;\n    }\n\n    get seoNamePre() {\n        return this.pathname.split(this.seoNameUrl)[0];\n    }\n\n    get seoNamePost() {\n        return this.pathname.split(this.seoNameUrl).slice(-1)[0]; // at least the -id theorically\n    }\n\n    get pathname() {\n        return new URL(this.props.url).pathname;\n    }\n\n    get url() {\n        if (this.seoContext.seoName) {\n            return this.props.url.replace(this.seoNameUrl, this.seoContext.seoName);\n        }\n        return this.props.url.replace(this.seoNameUrl, this.props.seoNameDefault);\n    }\n\n    get title() {\n        return this.seoContext.title || this.props.defaultTitle;\n    }\n\n    get description() {\n        return this.seoContext.description || this.props.previewDescription;\n    }\n\n    get descriptionWarning() {\n        if (!this.seoContext.description) {\n            return false;\n        }\n        if (this.seoContext.description.length < this.minRecommendedDescriptionSize) {\n            return _t(\"Your description looks too short.\");\n        } else if (this.seoContext.description.length > this.maxRecommendedDescriptionSize) {\n            return _t(\"Your description looks too long.\");\n        }\n        return false;\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {InputEvent} ev\n     */\n    _updateInputValue(ev) {\n        // `NFKD` as in `http_routing` python `slugify()`\n        ev.target.value = ev.target.value.trim().normalize('NFKD').toLowerCase()\n            .replace(/\\s+/g, '-') // Replace spaces with -\n            .replace(/[^\\w-]+/g, '') // Remove all non-word chars\n            .replace(/--+/g, '-'); // Replace multiple - with single -\n        this.seoContext.seoName = ev.target.value;\n    }\n}\n\nexport class OptimizeSEODialog extends Component {\n    static template = \"website.OptimizeSEODialog\";\n    static components = {\n        WebsiteDialog,\n        TitleDescription,\n        ImageSelector,\n        MetaKeywords,\n    };\n    static props = {\n        close: Function,\n    };\n\n    setup() {\n        this.website = useService('website');\n        this.dialogs = useService('dialog');\n        this.orm = useService('orm');\n\n        this.title = _t(\"Optimize SEO\");\n        this.saveButton = _t(\"Save\");\n        this.size = 'lg';\n        this.contentClass = \"oe_seo_configuration\";\n\n        onWillStart(async () => {\n            const { metadata: { mainObject, seoObject, path } } = this.website.currentWebsite;\n\n            this.object = seoObject || mainObject;\n            this.data = await rpc('/website/get_seo_data', {\n                'res_id': this.object.id,\n                'res_model': this.object.model,\n            });\n\n            this.canEditSeo = this.data.can_edit_seo;\n            this.canEditDescription = this.canEditSeo && 'website_meta_description' in this.data;\n            this.canEditTitle = this.canEditSeo && 'website_meta_title' in this.data;\n            this.canEditUrl = this.canEditSeo && 'seo_name' in this.data;\n            seoContext.title = this.canEditTitle && this.data.website_meta_title;\n\n            // If website.page, hide the google preview & tell user his page is currently unindexed\n            this.isIndexed = 'website_indexed' in this.data ? this.data.website_indexed : true;\n            this.seoNameHelp = _t(\"This value will be escaped to be compliant with all major browsers and used in url. Keep it empty to use the default name of the record.\");\n            this.previousSeoName = this.canEditUrl && this.data.seo_name;\n            seoContext.seoName = this.previousSeoName;\n            this.seoNameDefault = this.canEditUrl && this.data.seo_name_default;\n\n            seoContext.description = this.getMeta({ name: 'description' });\n            this.previewDescription = _t(\"The description will be generated by search engines based on page content unless you specify one.\");\n            this.defaultTitle = this.getMeta({ name: 'default_title' });\n            seoContext.defaultTitle = this.defaultTitle;\n            this.url = path;\n\n            seoContext.metaImage = this.data.website_meta_og_img || this.getMeta({ property: 'og:image' });\n\n            this.pageImages = this.getImages();\n            this.socialPreviewDescription = _t(\"The description will be generated by social media based on page content unless you specify one.\");\n            this.hasSocialDefaultImage = this.data.has_social_default_image;\n\n            this.canEditKeywords = 'website_meta_keywords' in this.data;\n            seoContext.keywords = this.getMeta({ name: 'keywords' });\n        });\n    }\n\n    get pageDocumentElement() {\n        return this.website.pageDocument.documentElement;\n    }\n\n    getImages() {\n        const imageEls = this.pageDocumentElement.querySelectorAll('#wrap img');\n        return [...new Set(Array.from(imageEls)\n                .filter(img => img.naturalHeight > 200 && img.naturalWidth > 200)\n                .map(({src}) => (src))\n            )];\n    }\n\n    getMeta({ name, property }) {\n        let query = '';\n        if (name) {\n            query = `meta[name=\"${name}\"]`;\n        }\n        if (property) {\n            query = `meta[property=\"${property}\"]`;\n        }\n        const el = this.pageDocumentElement.querySelector(query);\n        if (name === 'keywords') {\n            // Keywords might contain spaces which makes them fail the content\n            // check. Trim the strings to prevent this from happening.\n            const parsed = el && el.content.split(',').map(kw => kw.trim());\n            return parsed && parsed[0] ? [...new Set(parsed)] : [];\n        }\n        return el && el.content;\n    }\n\n    async save() {\n        const data = {};\n        if (this.canEditTitle) {\n            data.website_meta_title = seoContext.title;\n        }\n        if (this.canEditDescription) {\n            data.website_meta_description = seoContext.description;\n        }\n        if (this.canEditKeywords) {\n            data.website_meta_keywords = seoContext.keywords.join(',');\n        }\n        if (this.canEditUrl) {\n            if (seoContext.seoName !== this.previousSeoName) {\n                data.seo_name = seoContext.seoName;\n            }\n        }\n        data.website_meta_og_img = seoContext.metaImage;\n        await this.orm.write(this.object.model, [this.object.id], data, {\n            context: {\n                lang: this.website.currentWebsite.metadata.lang,\n                'website_id': this.website.currentWebsite.id,\n            },\n        });\n        this.website.goToWebsite({path: this.url.replace(this.previousSeoName || this.seoNameDefault, seoContext.seoName)});\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from '@web/core/utils/hooks';\nimport {\n    markup,\n    Component,\n    useState,\n    useEffect,\n    onWillStart,\n    onMounted,\n    onWillUnmount,\n} from \"@odoo/owl\";\n\nexport class WebsiteEditorComponent extends Component {\n    static template = \"website.WebsiteEditorComponent\";\n    static props = [\"*\"];\n    /**\n     * @override\n     */\n    setup() {\n        this.websiteService = useService('website');\n        this.notificationService = useService('notification');\n\n        this.websiteContext = useState(this.websiteService.context);\n        this.state = useState({\n            reloading: false,\n            showWysiwyg: false,\n        });\n        this.wysiwygOptions = {};\n\n        onWillStart(async () => {\n            await this.websiteService.loadWysiwyg();\n            const adapterModule = await odoo.loader.modules.get('@website/components/wysiwyg_adapter/wysiwyg_adapter');\n            this.WysiwygAdapterComponent = adapterModule.WysiwygAdapterComponent;\n        });\n\n        useEffect(isPublicRootReady => {\n            if (isPublicRootReady) {\n                this.publicRootReady();\n            }\n        }, () => [this.websiteContext.isPublicRootReady]);\n\n        onMounted(() => {\n            this.websiteService.blockPreview(false);\n            if (this.websiteContext.isPublicRootReady) {\n                this.publicRootReady();\n            }\n        });\n\n        onWillUnmount(() => {\n            if (this.onWillUnmount) {\n                this.onWillUnmount();\n            }\n        });\n    }\n    /**\n     * Starts the wysiwyg or disable edition if currently\n     * on a translated page.\n     */\n    publicRootReady() {\n        if (this.websiteService.currentWebsite.metadata.translatable) {\n            this.websiteContext.edition = false;\n            this.websiteService.unblockPreview();\n        } else {\n            this.state.showWysiwyg = true;\n        }\n    }\n    /**\n     * Displays the side menu and unblock the iframe to\n     * start edition.\n     */\n    wysiwygReady() {\n        this.websiteContext.snippetsLoaded = true;\n        if (this.state.reloading) {\n            document.body.classList.remove('editor_has_dummy_snippets');\n            this.state.reloading = false;\n        }\n        this.websiteService.unblockPreview();\n        document.body.classList.add(\"o_website_navbar_transition_hide\");\n        setTimeout(() => document.body.classList.add(\"o_website_navbar_hide\"), 400);\n    }\n    /**\n     * Prepares the editor for reload. Copies the widget element tree\n     * to display it as a skeleton so that it doesn't flash when the editor\n     * is destroyed and re-started.\n     *\n     * @param widgetEl {HTMLElement} Widget element of the editor to copy.\n     */\n    willReload(widgetEl) {\n        this.websiteService.blockPreview();\n        if (widgetEl) {\n            this.loadingDummy = markup(widgetEl.innerHTML);\n        }\n        this.state.reloading = true;\n        document.body.classList.add('editor_has_dummy_snippets');\n    }\n    /**\n     * Dismount the editor and reload the iframe.\n     *\n     * @param snippetOptionSelector {string} Selector to refocus the editor once reloaded.\n     * @param [url] {string} URL to reload the iframe tp\n     * @returns {Promise<void>}\n     */\n    async reload({ snippetOptionSelector, url } = {}) {\n        this.notificationService.add(_t(\"Your modifications were saved to apply this option.\"), {\n            title: _t(\"Content saved.\"),\n            type: 'success'\n        });\n        this.state.showWysiwyg = false;\n        await this.props.reloadIframe(url);\n        this.reloadSelector = snippetOptionSelector;\n    }\n    /**\n     * Blocks the iframe and start the hiding transition.\n     * @param {Boolean} [reloadIframe=true]\n     * @param {Function} onLeave A callback that will be played after the\n     * transition, when the component is unmounted.\n     * @returns {Promise<void>}\n     */\n    async quit({ reloadIframe = true, onLeave } = {}) {\n        this.onWillUnmount = onLeave;\n        if (reloadIframe) {\n            this.websiteService.blockPreview();\n            await this.props.reloadIframe();\n            this.websiteService.unblockPreview();\n        }\n        this.websiteContext.snippetsLoaded = false;\n        document.body.classList.remove(\"o_website_navbar_hide\");\n        setTimeout(() => document.body.classList.remove(\"o_website_navbar_transition_hide\"));\n        setTimeout(this.destroyAfterTransition.bind(this), 400);\n    }\n    /**\n     * Dismounts the editor.\n     *\n     * @returns {Promise<void>}\n     */\n    destroyAfterTransition() {\n        this.state.showWysiwyg = false;\n        this.websiteContext.edition = false;\n    }\n}\n", "/** @odoo-module **/\n\nimport { NavBar } from '@web/webclient/navbar/navbar';\nimport { useService, useBus } from '@web/core/utils/hooks';\nimport { registry } from \"@web/core/registry\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { UserMenu } from \"@web/webclient/user_menu/user_menu\";\nimport { useEffect } from \"@odoo/owl\";\n\nconst websiteSystrayRegistry = registry.category('website_systray');\nwebsiteSystrayRegistry.add(\"UserMenu\", { Component: UserMenu }, { sequence: 14 });\n\npatch(NavBar.prototype, {\n    setup() {\n        super.setup();\n        this.websiteService = useService('website');\n        this.websiteCustomMenus = useService('website_custom_menus');\n\n        // The navbar is rerendered with an event, as it can not naturally be\n        // with props/state (the WebsitePreview client action and the navbar\n        // are not related).\n        useBus(websiteSystrayRegistry, 'EDIT-WEBSITE', () => this.render(true));\n\n        if (this.env.debug && !websiteSystrayRegistry.contains('web.debug_mode_menu')) {\n            websiteSystrayRegistry.add('web.debug_mode_menu', registry.category('systray').get('web.debug_mode_menu'), {sequence: 100});\n        }\n        // Similar to what is done in web/navbar. When the app menu or systray\n        // is updated, we need to adapt the navbar so that the \"more\" menu\n        // can be computed.\n        let adaptCounter = 0;\n        const renderAndAdapt = () => {\n            this.render(true);\n            adaptCounter++;\n        };\n        useEffect(\n            (adaptCounter) => {\n                // We do not want to adapt on the first render\n                // as the super class already does it.\n                if (adaptCounter > 0) {\n                    this.adapt();\n                }\n            },\n            () => [adaptCounter]\n        );\n\n        useBus(websiteSystrayRegistry, 'CONTENT-UPDATED', renderAndAdapt);\n    },\n\n    get shouldDisplayWebsiteSystray() {\n        return this.websiteService.currentWebsite && this.websiteService.isRestrictedEditor;\n    },\n\n    // Somehow a setter is needed in `patch()` to avoid an owl error.\n    set shouldDisplayWebsiteSystray(_) {},\n\n    /**\n     * @override\n     */\n    get systrayItems() {\n        if (this.websiteService.currentWebsite) {\n            const websiteItems = websiteSystrayRegistry\n                .getEntries()\n                .map(([key, value], index) => ({ key, ...value, index }))\n                .filter((item) => ('isDisplayed' in item ? item.isDisplayed(this.env) : true))\n                .reverse();\n            // Do not override the regular Odoo navbar if the only visible\n            // elements are the debug items.\n            if (!websiteItems.every((item) => ['burger_menu', 'web.debug_mode_menu'].includes(item.key))) {\n                return websiteItems;\n            }\n        }\n        return super.systrayItems;\n    },\n\n    /**\n     * @override\n     */\n    get currentAppSections() {\n        const currentAppSections = super.currentAppSections;\n        if (this.currentApp && this.currentApp.xmlid === 'website.menu_website_configuration') {\n            return this.websiteCustomMenus.addCustomMenus(currentAppSections).filter(section => section.childrenTree.length);\n        }\n        return currentAppSections;\n    },\n\n    /**\n     * @override\n     */\n    async onNavBarDropdownItemSelection(menu) {\n        const websiteMenu = this.websiteCustomMenus.get(menu.xmlid);\n        if (websiteMenu) {\n            return this.websiteCustomMenus.open(menu);\n        }\n        return super.onNavBarDropdownItemSelection(menu);\n    },\n});\n", "/** @odoo-module **/\n\nimport { BurgerMenu } from '@web/webclient/burger_menu/burger_menu';\nimport { useService } from '@web/core/utils/hooks';\nimport { registry } from \"@web/core/registry\";\nimport { patch } from \"@web/core/utils/patch\";\n\nconst websiteSystrayRegistry = registry.category('website_systray');\n\npatch(BurgerMenu.prototype, {\n    setup() {\n        super.setup();\n        this.websiteCustomMenus = useService('website_custom_menus');\n\n        if (!websiteSystrayRegistry.contains('burger_menu')) {\n            websiteSystrayRegistry.add('burger_menu', registry.category('systray').get('burger_menu'), {sequence: 0});\n        }\n    },\n\n    /**\n     * @override\n     */\n    get currentAppSections() {\n        const currentAppSections = super.currentAppSections;\n        if (this.currentApp && this.currentApp.xmlid === 'website.menu_website_configuration') {\n            return this.websiteCustomMenus.addCustomMenus(currentAppSections).filter(section => section.childrenTree.length);\n        }\n        return currentAppSections;\n    },\n\n    /**\n     * This dummy setter is only here to prevent conflicts between the\n     * Enterprise BurgerMenue extension and the Website BurgerMenu patch.\n     */\n    set currentAppSections(_) {},\n\n    /**\n     * @override\n     */\n    async _onMenuClicked(menu) {\n        const websiteMenu = this.websiteCustomMenus.get(menu.xmlid);\n        if (websiteMenu) {\n            await this.websiteCustomMenus.open(menu);\n            this._closeBurger();\n        } else {\n            super._onMenuClicked(menu);\n        }\n    },\n});\n", "/** @odoo-module **/\n\nimport { Component, xml } from \"@odoo/owl\";\n\nconst NO_OP = () => {};\n\nexport class Switch extends Component {\n    static props = {\n        value: Boolean,\n        extraClasses: String,\n        disabled: { type: Boolean, optional: true },\n        label: { type: String, optional: true },\n        onChange: { Function, optional: true },\n    };\n    static defaultProps = {\n        onChange: NO_OP,\n    };\n    static template = xml`\n    <label t-att-class=\"'o_switch' + extraClasses\">\n        <input type=\"checkbox\" t-att-checked=\"props.value\" t-att-disabled=\"props.disabled\" t-on-change=\"(ev) => props.onChange(ev.target.checked)\"/>\n        <span/>\n        <span t-if=\"props.label\" t-esc=\"props.label\" class=\"ms-2\"/>\n    </label>\n    `;\n\n    setup() {\n        this.extraClasses = this.props.extraClasses ? ` ${this.props.extraClasses}` : '';\n    }\n}\n", "/** @odoo-module **/\nexport const pageOptionsCallbacks = {\n    header_overlay: function (value) {\n        this.document.getElementById('wrapwrap').classList.toggle('o_header_overlay', value);\n    },\n    header_visible: function (value) {\n        const headerEl = this.document.querySelector('#wrapwrap > header');\n        headerEl.classList.toggle('d-none', !value);\n        headerEl.classList.toggle('o_snippet_invisible', !value);\n    },\n    footer_visible: function (value) {\n        const footerEl = this.document.querySelector('#wrapwrap > footer');\n        footerEl.classList.toggle('d-none', !value);\n        footerEl.classList.toggle('o_snippet_invisible', !value);\n    },\n};\nexport class PageOption {\n    /***\n     * Some page options are defined with an input el hidden inside the content that we're editing.\n     *\n     * @param {HTMLInputElement} el The element holding the value of the page option\n     * @param {Document} document The document on which the option applies.\n     * @param {string} name The name of the method to be called before applying an option.\n     * @param {boolean} isDirty true when it has been modified during an edit session.\n     */\n    constructor(el, document, name, isDirty = false) {\n        this.el = el;\n        this.isDirty = isDirty;\n        this.document = document;\n        this.name = name;\n        if (pageOptionsCallbacks[name]) {\n            this.callback = pageOptionsCallbacks[name].bind(this);\n        }\n    }\n    get value() {\n        if (this.el.value.toLowerCase() === 'true') {\n            return true;\n        } else if (this.el.value.toLowerCase() === 'false') {\n            return false;\n        }\n        return this.el.value;\n    }\n    set value(value) {\n        if (this.callback) {\n            this.callback(value);\n        }\n        this.el.value = value;\n        this.isDirty = true;\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from '@web/core/utils/hooks';\nimport { WebsiteEditorComponent } from '../editor/editor';\nimport { WebsiteDialog } from '../dialog/dialog';\nimport { browser } from \"@web/core/browser/browser\";\nimport { useEffect, useRef, Component, xml } from \"@odoo/owl\";\n\nconst localStorageNoDialogKey = 'website_translator_nodialog';\n\nexport class AttributeTranslateDialog extends Component {\n    static components = { WebsiteDialog };\n    static template = \"website.AttributeTranslateDialog\";\n    static props = [\"node\", \"close\"];\n    setup() {\n        this.title = _t(\"Translate Attribute\");\n\n        this.formEl = useRef('form-container');\n\n        useEffect(() => {\n            this.translation = $(this.props.node).data('translation');\n            const $group = $('<div/>', {class: 'mb-3'}).appendTo(this.formEl.el);\n            for (const [attr, node] of Object.entries(this.translation)) {\n                const $node = $(node);\n                const $label = $('<label class=\"col-form-label\"></label>').text(attr);\n                const $input = $('<input class=\"form-control\"/>').val($node.html());\n                $input.on('change keyup', function () {\n                    const value = $input.val();\n                    $node.text(value).trigger('change', node);\n                    const $originalNode = $node.data('$node');\n                    const nodeAttribute = $node.data('attribute');\n                    if (nodeAttribute) {\n                        $originalNode.attr(nodeAttribute, value);\n                        if (nodeAttribute === 'value') {\n                            $originalNode[0].value = value;\n                        }\n                        $originalNode.trigger('translate');\n                    } else {\n                        $originalNode.val(value).trigger('translate');\n                    }\n                    $node.trigger('change');\n                    $originalNode[0].classList.add('oe_translated');\n                });\n                $group.append($label).append($input);\n            }\n        }, () => [this.props.node]);\n    }\n}\n\n// Used to translate the text of `<select/>` options since it should not be\n// possible to interact with the content of `.o_translation_select` elements.\nexport class SelectTranslateDialog extends Component {\n    static components = { WebsiteDialog };\n    static template = xml`\n    <WebsiteDialog close=\"props.close\"\n        title=\"title\"\n        showSecondaryButton=\"false\">\n        <input\n            t-ref=\"input\"\n            type=\"text\" class=\"form-control my-3\"\n            t-att-value=\"optionEl.textContent or ''\"\n            t-on-keyup=\"onInputKeyup\"/>\n    </WebsiteDialog>\n    `;\n    static props = {\n        node: String,\n        close: Function,\n    };\n    setup() {\n        this.title = _t(\"Translate Selection Option\");\n        this.inputEl = useRef('input');\n        this.optionEl = this.props.node;\n    }\n\n    onInputKeyup() {\n        const value = this.inputEl.el.value;\n        this.optionEl.textContent = value;\n        this.optionEl.classList.toggle(\n            'oe_translated',\n            value !== this.optionEl.dataset.initialTranslationValue\n        );\n    }\n}\n\nexport class TranslatorInfoDialog extends Component {\n    static components = { WebsiteDialog };\n    static template = \"website.TranslatorInfoDialog\";\n    static props = {\n        close: Function,\n    };\n    setup() {\n        this.strongOkButton = _t(\"Ok, never show me this again\");\n        this.okButton = _t(\"Ok\");\n    }\n\n    onStrongOkClick() {\n        browser.localStorage.setItem(localStorageNoDialogKey, true);\n    }\n}\n\nconst savableSelector = '[data-oe-translation-source-sha], ' +\n    '[data-oe-model][data-oe-id][data-oe-field], ' +\n    '[placeholder*=\"data-oe-translation-source-sha=\"], ' +\n    '[title*=\"data-oe-translation-source-sha=\"], ' +\n    '[value*=\"data-oe-translation-source-sha=\"], ' +\n    'textarea:contains(data-oe-translation-source-sha), ' +\n    '[alt*=\"data-oe-translation-source-sha=\"]';\n\nexport class WebsiteTranslator extends WebsiteEditorComponent {\n    setup() {\n        super.setup();\n\n        this.dialogService = useService('dialog');\n\n        this.wysiwygOptions.enableTranslation = true;\n        this.wysiwygOptions.devicePreview = false;\n\n        this.editableElements = (...args) => this._editableElements(...args);\n        this.beforeEditorActive = (...args) => this._beforeEditorActive(...args);\n    }\n\n    /**\n     * @override\n     */\n    publicRootReady() {\n        if (!this.websiteService.currentWebsite.metadata.translatable) {\n            this.websiteContext.translation = false;\n        } else {\n            this.state.showWysiwyg = true;\n            this.deleteQueryParam(\"edit_translations\", this.websiteService.contentWindow, true);\n        }\n    }\n\n    /**\n     * @override\n     */\n    destroyAfterTransition() {\n        this.state.showWysiwyg = false;\n        this.websiteContext.translation = false;\n    }\n\n    get savableSelector() {\n        return savableSelector;\n    }\n\n    getEditableArea() {\n        return this.$wysiwygEditable.find(':o_editable').add(this.$editables);\n    }\n\n    _editableElements() {\n        return $(this.websiteService.pageDocument).find(savableSelector)\n            .not('[data-oe-readonly]');\n    }\n\n    getTranslationObject(nodeEl) {\n        const { oeModel, oeId, oeField, oeTranslationInitialMd5 } = nodeEl.dataset;\n        const id = [oeModel, oeId, oeField, oeTranslationInitialMd5].join(',');\n        let translation = this.translations.filter(t => t.id === id)[0];\n        if (!translation) {\n            translation = { id };\n            this.translations.push(translation);\n        }\n        return translation;\n    }\n\n    async _beforeEditorActive($wysiwygEditable) {\n        this.$wysiwygEditable = $wysiwygEditable;\n        const self = this;\n        var attrs = ['placeholder', 'title', 'alt', 'value'];\n        const $editable = this.getEditableArea();\n        const translationRegex = /<span [^>]*data-oe-translation-source-sha=\"([^\"]+)\"[^>]*>(.*)<\\/span>/;\n        let $edited = $();\n        attrs.forEach((attr) => {\n            const attrEdit = $editable.filter('[' + attr + '*=\"data-oe-translation-source-sha=\"]').filter(':empty, input, select, textarea, img');\n            attrEdit.each(function () {\n                var $node = $(this);\n                var translation = $node.data('translation') || {};\n                var trans = $node.attr(attr);\n                var match = trans.match(translationRegex);\n                var $trans = $(trans).addClass('d-none o_editable o_editable_translatable_attribute').appendTo(self.websiteService.pageDocument.body);\n                $trans.data('$node', $node).data('attribute', attr);\n\n                translation[attr] = $trans[0];\n                $node.attr(attr, match[2]);\n                // Using jQuery attr() to update the \"value\" will not change\n                // what appears in the DOM and will not update the value\n                // property on inputs. We need to force the right value instead\n                // of the original translation <span/>.\n                if (attr === 'value') {\n                    $node[0].value = match[2];\n                }\n\n                $node.addClass('o_translatable_attribute').data('translation', translation);\n            });\n            $edited = $edited.add(attrEdit);\n        });\n        const textEdit = $editable.filter('textarea:contains(data-oe-translation-source-sha)');\n        textEdit.each(function () {\n            var $node = $(this);\n            var translation = $node.data('translation') || {};\n            var trans = $node.text();\n            var match = trans.match(translationRegex);\n            var $trans = $(trans).addClass('d-none o_editable o_editable_translatable_text').appendTo(self.websiteService.pageDocument.body);\n            $trans.data('$node', $node);\n\n            translation['textContent'] = $trans[0];\n            $node.val(match[2]);\n            // Update the text content of textarea too.\n            $node[0].innerText = match[2];\n\n            $node.addClass('o_translatable_text').removeClass('o_text_content_invisible')\n                .data('translation', translation);\n        });\n\n        // Hack: we add a temporary element to handle option's text\n        // translations from the linked <select/>. The final values are\n        // copied to the original element right before save.\n        $editable.filter('[data-oe-translation-source-sha] > select').each((index, select) => {\n            const selectTranslationEl = document.createElement('div');\n            selectTranslationEl.className = 'o_translation_select';\n            const optionNames = [...select.options].map(option => option.text);\n            optionNames.forEach(option => {\n                const optionEl = document.createElement('div');\n                optionEl.textContent = option;\n                optionEl.dataset.initialTranslationValue = option;\n                optionEl.className = 'o_translation_select_option';\n                selectTranslationEl.appendChild(optionEl);\n            });\n            select.before(selectTranslationEl);\n        });\n\n        this.translations = [];\n        this.$translations = this.getEditableArea().filter('.o_translatable_attribute, .o_translatable_text');\n        this.$editables = $(this.websiteService.pageDocument).find('.o_editable_translatable_attribute, .o_editable_translatable_text');\n\n        this.markTranslatableNodes();\n        this.$translations.filter('input[type=hidden].o_translatable_input_hidden').prop('type', 'text');\n\n        // We don't want the BS dropdown to close\n        // when clicking in a element to translate\n        $(this.websiteService.pageDocument).find('.dropdown-menu').on('click', '.o_editable', function (ev) {\n            ev.stopPropagation();\n            ev.preventDefault();\n        });\n\n        if (!browser.localStorage.getItem(localStorageNoDialogKey)) {\n            this.dialogService.add(TranslatorInfoDialog);\n        }\n\n        // Apply data-oe-readonly on nested data.\n        $(this.websiteService.pageDocument).find(savableSelector)\n            .filter(':has(' + savableSelector + ')')\n            .attr('data-oe-readonly', true);\n\n        const styleEl = document.createElement('style');\n        styleEl.id = \"translate-stylesheet\";\n        this.websiteService.pageDocument.head.appendChild(styleEl);\n\n        const toTranslateColor = window.getComputedStyle(document.documentElement).getPropertyValue('--o-we-content-to-translate-color');\n        const translatedColor = window.getComputedStyle(document.documentElement).getPropertyValue('--o-we-translated-content-color');\n\n        styleEl.sheet.insertRule(`[data-oe-translation-state].o_dirty {background: ${translatedColor} !important;}`);\n        styleEl.sheet.insertRule(`[data-oe-translation-state=\"translated\"] {background: ${translatedColor} !important;}`);\n        styleEl.sheet.insertRule(`[data-oe-translation-state] {background: ${toTranslateColor} !important;}`);\n\n        const showNotification = ev => {\n            let message = _t('This translation is not editable.');\n            if (ev.target.closest('.s_table_of_content_navbar_wrap')) {\n                message = _t('Translate header in the text. Menu is generated automatically.');\n            }\n            this.env.services.notification.add(message, {\n                type: 'info',\n                sticky: false,\n            });\n        };\n        for (const translationEl of $editable) {\n            if (translationEl.closest('.o_not_editable')) {\n                translationEl.addEventListener('click', showNotification);\n            }\n            if (translationEl.closest('.s_table_of_content_navbar_wrap')) {\n                // Make sure the same translation ids are used.\n                const href = translationEl.closest('a').getAttribute('href');\n                const headerEl = translationEl.closest('.s_table_of_content').querySelector(`${href} [data-oe-translation-source-sha]`);\n                if (headerEl) {\n                    if (translationEl.dataset.oeTranslationSourceSha !== headerEl.dataset.oeTranslationSourceSha) {\n                        // Use the same identifier for the generated navigation\n                        // label and its associated header so that the general\n                        // synchronization mechanism kicks in.\n                        // The initial value is kept to be restored before save\n                        // in order to keep the translation of the unstyled\n                        // label distinct from the one of the header.\n                        translationEl.dataset.oeTranslationSaveSha = translationEl.dataset.oeTranslationSourceSha;\n                        translationEl.dataset.oeTranslationSourceSha = headerEl.dataset.oeTranslationSourceSha;\n                    }\n                    translationEl.classList.add('o_translation_without_style');\n                }\n            }\n        }\n    }\n\n    markTranslatableNodes() {\n        const self = this;\n        this.getEditableArea().each(function () {\n            var $node = $(this);\n            const translation = self.getTranslationObject(this);\n            translation.value = (translation.value ? translation.value : $node.html()).replace(/[ \\t\\n\\r]+/, ' ');\n        });\n\n        // attributes\n\n        this.$translations.each(function () {\n            var $node = $(this);\n            var translation = $node.data('translation');\n            Object.values(translation).forEach((node) => {\n                var trans = self.getTranslationObject(node);\n                trans.value = (trans.value ? trans.value : $node.html()).replace(/[ \\t\\n\\r]+/, ' ');\n                trans.state = node.dataset.oeTranslationState;\n                // If a node has an already translated attribute, we don't\n                // need to update its state, since it can be set again as\n                // \"to_translate\" by other attributes...\n                if ($node[0].dataset.oeTranslationState === 'translated') {\n                    return;\n                }\n                $node.attr('data-oe-translation-state', (trans.state || 'to_translate'));\n            });\n        });\n\n        this.$translations\n            .add(this.getEditableArea().filter('.o_translation_select_option'))\n            .prependEvent('click.translator', (ev) => {\n                const node = ev.target;\n                const isSelectTranslation = !!node.closest('.o_translation_select');\n                this.dialogService.add(isSelectTranslation ?\n                    SelectTranslateDialog : AttributeTranslateDialog, {node});\n            });\n    }\n\n    _onSave(ev) {\n        ev.stopPropagation();\n    }\n\n    deleteQueryParam(param, target = window, adaptBrowserUrl = false) {\n        const url = new URL(target.location.href);\n        url.searchParams.delete(param);\n        target.history.replaceState(target.history.state, null, url);\n        if (adaptBrowserUrl) {\n            this.deleteQueryParam(param);\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport {formView} from \"@web/views/form/form_view\";\nimport {registry} from \"@web/core/registry\";\n\nexport class NewContentFormController extends formView.Controller {\n    /**\n     * @override\n     */\n    async save() {\n        return super.save({ computePath: () => this.computePath(), ...arguments });\n    }\n\n    /**\n     * Returns the URL to redirect to once the website content (blog, etc)\n     * record is created.\n     * Override this method to get the correct path for records without\n     * 'website_url' field.\n     *\n     * @returns {String}\n     */\n    computePath() {\n        return this.model.root.data.website_url;\n    }\n}\n\nexport const NewContentFormView = {\n    ...formView,\n    display: {controlPanel: false},\n    Controller: NewContentFormController,\n};\n\nregistry.category(\"views\").add(\"website_new_content_form\", NewContentFormView);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { EditMenuDialog } from '@website/components/dialog/edit_menu';\nimport { OptimizeSEODialog } from '@website/components/dialog/seo';\nimport {PagePropertiesDialog} from '@website/components/dialog/page_properties';\n\n/**\n * This service displays contextual menus, depending of the state of the\n * website. These menus are defined in xml with the \"website_preview\" action,\n * which is overriden here for displaying dialogs, or regular components that\n * are not client actions.\n */\nexport const websiteCustomMenus = {\n    dependencies: ['website', 'orm', 'dialog', 'ui'],\n    start(env, { website, orm, dialog, ui }) {\n        const services = { website, orm, dialog, ui };\n        return {\n            get(xmlId) {\n                return registry.category('website_custom_menus').get(xmlId, null);\n            },\n            async open(customMenu) {\n                const menuConfig = this.get(customMenu.xmlid);\n                if (menuConfig.openWidget) {\n                    return menuConfig.openWidget(services);\n                }\n                const menuProps = {\n                    ...(menuConfig.getProps && (await menuConfig.getProps(services))),\n                    // Values on 'dynamicProps' are retrieved after the content is loaded (e.g. id of\n                    // the content menu to be edited).\n                    ...customMenu.dynamicProps,\n                };\n                return dialog.add(\n                    menuConfig.Component,\n                    menuProps,\n                );\n            },\n            addCustomMenus(sections) {\n                const filteredSections = [];\n                for (const section of sections) {\n                    const isWebsiteCustomMenu = !!this.get(section.xmlid);\n                    const displayWebsiteCustomMenu = isWebsiteCustomMenu && website.isRestrictedEditor && this.get(section.xmlid).isDisplayed(env);\n                    if (!isWebsiteCustomMenu || displayWebsiteCustomMenu) {\n                        let subSections = [];\n                        if (section.childrenTree.length) {\n                            subSections = this.addCustomMenus(section.childrenTree);\n                        }\n                        if (section.xmlid === 'website.custom_menu_edit_menu') {\n                            // Hack: this code will simulate an XML pre-configured navbar menuitem to edit each\n                            // content menu found on the current page by duplicating one menuitem with\n                            // different data (name, dialog props...). this will prevent breaking the current\n                            // 'navbar menus' display system.\n                            filteredSections.push(...website.currentWebsite.metadata.contentMenus.map((menu, index) => ({\n                                ...section,\n                                name: _t(\"Edit %s\", menu[0]),\n                                dynamicProps: {rootID: parseInt(menu[1], 10)},\n                                // Prevent a 't-foreach' duplicate key on menus template.\n                                id: `${section.id}-${index}`,\n                            })));\n                        } else {\n                            filteredSections.push(Object.assign({}, section, {childrenTree: subSections}));\n                        }\n                    }\n                }\n                for (const section of filteredSections) {\n                    section.childrenTree = section.childrenTree.filter(\n                        // Exclude non-leaf node having no visible sub-element.\n                        tree => !(tree.children.length && !tree.childrenTree.length)\n                    );\n                }\n                return filteredSections;\n            },\n        };\n    }\n};\nregistry.category('services').add('website_custom_menus', websiteCustomMenus);\n\nregistry.category('website_custom_menus').add('website.menu_edit_menu', {\n    Component: EditMenuDialog,\n    isDisplayed: (env) => !!env.services.website.currentWebsite\n        && env.services.website.isDesigner\n        && !env.services.website.currentWebsite.metadata.translatable,\n});\nregistry.category('website_custom_menus').add('website.menu_optimize_seo', {\n    Component: OptimizeSEODialog,\n    isDisplayed: (env) => env.services.website.currentWebsite\n        && env.services.website.isRestrictedEditor\n        && !!env.services.website.currentWebsite.metadata.canOptimizeSeo,\n});\nregistry.category('website_custom_menus').add('website.menu_ace_editor', {\n    openWidget: (services) => services.website.context.showResourceEditor = true,\n    isDisplayed: (env) => env.services.website.currentWebsite\n        && env.services.website.currentWebsite.metadata.viewXmlid\n        && !env.services.ui.isSmall,\n});\nregistry.category('website_custom_menus').add('website.menu_page_properties', {\n    Component: PagePropertiesDialog,\n    isDisplayed: (env) => env.services.website.currentWebsite\n        && env.services.website.isDesigner\n        && !!env.services.website.currentWebsite.metadata.mainObject,\n    getProps: async ({ orm, website }) => {\n        const mainObject = website.currentWebsite.metadata.mainObject;\n        const isPage = mainObject.model === \"website.page\";\n        const model = isPage ? \"website.page.properties\" : \"website.page.properties.base\";\n        return {\n            resId: await orm.call(model, \"create\", [\n                isPage\n                    ? {\n                          target_model_id: mainObject.id,\n                          website_id: website.currentWebsite.id,\n                      }\n                    : {\n                          target_model_id: `${mainObject.model},${mainObject.id}`,\n                          url: window.location.pathname,\n                          website_id: website.currentWebsite.id,\n                      },\n            ]),\n            resModel: model,\n            onRecordSaved: async (record) => {\n                const page = isPage\n                    ? (await orm.read(\"website.page\", [mainObject.id], [\"website_id\", \"url\"]))[0]\n                    : undefined;\n                return website.goToWebsite({\n                    websiteId: page?.website_id?.[0] ?? website.currentWebsite.id,\n                    path: page?.url ?? website.currentWebsite.metadata.path,\n                });\n            },\n        };\n    },\n});\nregistry.category('website_custom_menus').add('website.custom_menu_edit_menu', {\n    Component: EditMenuDialog,\n    // 'isDisplayed' === true => at least 1 content menu was found on the page. This\n    // menuitem will be cloned (in 'addCustomMenus()') to edit every content menu using\n    // the 'EditMenuDialog' component.\n    isDisplayed: (env) => env.services.website.currentWebsite\n        && env.services.website.currentWebsite.metadata.contentMenus\n        && env.services.website.currentWebsite.metadata.contentMenus.length,\n});\n", "/** @odoo-module **/\n\nimport {\n    changeBackgroundColor,\n    clickOnSnippet,\n    clickOnText,\n    insertSnippet,\n    goBackToBlocks,\n    registerThemeHomepageTour,\n} from \"@website/js/tours/tour_utils\";\n\nconst snippets = [\n    {\n        id: 's_banner',\n        name: 'Banner',\n        groupName: \"Intro\",\n    },\n    {\n        id: 's_three_columns',\n        name: 'Columns',\n        groupName: \"Columns\",\n    },\n    {\n        id: 's_text_image',\n        name: 'Image - Text',\n        groupName: \"Content\",\n    },\n    {\n        id: 's_masonry_block',\n        name: 'Masonry',\n        groupName: \"Images\",\n    },\n    {\n        id: 's_title',\n        name: 'Title',\n        groupName: \"Text\",\n    },\n    {\n        id: 's_showcase',\n        name: 'Showcase',\n        groupName: \"Content\",\n    },\n    {\n        id: 's_call_to_action',\n        name: 'Call to Action',\n        groupName: \"Content\",\n    },\n    {\n        id: 's_quotes_carousel',\n        name: 'Quotes',\n        groupName: \"People\",\n    },\n];\n\nregisterThemeHomepageTour('homepage', () => [\n    ...insertSnippet(snippets[0], \"top\"),\n    ...clickOnText(snippets[0], \"h1\"),\n    goBackToBlocks(),\n    ...insertSnippet(snippets[1]),\n    ...insertSnippet(snippets[2]),\n    ...clickOnSnippet(snippets[2], \"top\"),\n    changeBackgroundColor(),\n    goBackToBlocks(),\n    ...insertSnippet(snippets[3]),\n    ...insertSnippet(snippets[4], \"top\"),\n    ...insertSnippet(snippets[5]),\n    ...insertSnippet(snippets[6]),\n    ...insertSnippet(snippets[7]),\n]);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { useService, useBus } from \"@web/core/utils/hooks\";\nimport { Component, onWillStart, useState } from \"@odoo/owl\";\n\nconst websiteSystrayRegistry = registry.category('website_systray');\n\nexport class EditInBackendSystray extends Component {\n    static template = \"website.EditInBackendSystray\";\n    static props = {};\n    setup() {\n        this.websiteService = useService('website');\n        this.actionService = useService('action');\n        this.state = useState({mainObjectName: ''});\n\n        onWillStart(this._updateMainObjectName);\n        useBus(websiteSystrayRegistry, 'CONTENT-UPDATED', this._updateMainObjectName);\n    }\n\n    editInBackend() {\n        const { metadata: { mainObject } } = this.websiteService.currentWebsite;\n        this.actionService.doAction({\n            res_model: mainObject.model,\n            res_id: mainObject.id,\n            views: [[false, \"form\"]],\n            type: \"ir.actions.act_window\",\n            view_mode: \"form\",\n        });\n    }\n\n    async _updateMainObjectName() {\n        this.state.mainObjectName = await this.websiteService.getUserModelName();\n    }\n}\n\nexport const systrayItem = {\n    Component: EditInBackendSystray,\n    isDisplayed: env => env.services.website.currentWebsite && env.services.website.currentWebsite.metadata.editableInBackend,\n};\n\nregistry.category(\"website_systray\").add(\"EditInBackend\", systrayItem, { sequence: 10 });\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { useService } from '@web/core/utils/hooks';\nimport { Component, useState, useEffect } from \"@odoo/owl\";\n\nclass EditWebsiteSystray extends Component {\n    static template = \"website.EditWebsiteSystray\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n    };\n    static props = {};\n    setup() {\n        this.websiteService = useService('website');\n        this.websiteContext = useState(this.websiteService.context);\n\n        this.state = useState({\n            isLoading: false,\n        });\n\n        useEffect((edition) => {\n            if (edition) {\n                this.state.isLoading = true;\n            }\n        }, () => [this.websiteContext.edition]);\n\n        useEffect((snippetsLoaded) => {\n            if (snippetsLoaded) {\n                this.state.isLoading = false;\n            }\n        }, () => [this.websiteContext.snippetsLoaded]);\n    }\n\n    get translatable() {\n        return this.websiteService.currentWebsite\n            && this.websiteService.currentWebsite.metadata.translatable\n            && this.websiteContext.isPublicRootReady;\n    }\n\n    get label() {\n        return _t(\"Edit\");\n    }\n\n    async attemptStartTranslate() {\n        if (this.websiteService.isRestrictedEditor && !this.websiteService.isDesigner) {\n            const object = this.websiteService.currentWebsite.metadata.mainObject;\n            const objects = {\n                [object.model]: object.id,\n            };\n            const otherRecordEls = this.websiteService.websiteRootInstance.el.querySelectorAll(\n                \"[data-res-model][data-res-id]:not([data-res-model='ir.ui.view']), [data-oe-model][data-oe-id]:not([data-oe-model='ir.ui.view'])\"\n            );\n            for (const el of otherRecordEls) {\n                const model = el.dataset.resModel || el.dataset.oeModel;\n                if (!objects[model]) {\n                    // Keep one record of each type.\n                    objects[model] = parseInt(el.dataset.resId || el.dataset.oeId);\n                }\n            }\n            await rpc('/website/check_can_modify_any', {\n                records: Object.entries(objects).map(([res_model, res_id]) => ({\n                    res_model,\n                    res_id,\n                })),\n            })\n        }\n        this.startTranslate();\n    }\n\n    startTranslate() {\n        const { pathname, search, hash } = this.websiteService.contentWindow.location;\n        if (!search.includes('edit_translations')) {\n            const searchParams = new URLSearchParams(search);\n            searchParams.set('edit_translations', '1');\n            this.websiteService.goToWebsite({\n                path: pathname + `?${searchParams.toString() + hash}`,\n                translation: true\n            });\n        } else {\n            this.websiteContext.translation = true;\n        }\n    }\n\n    startEdit() {\n        if (this.translatable) {\n            // We are in translate mode, the pathname starts with '/<url_code>'. By\n            // adding a trailing slash we can simply search for the first slash\n            // after the language code to remove the language part.\n            const { pathname, search, hash } = this.websiteService.contentWindow.location;\n            const languagePrefix = `${pathname}/`.indexOf('/', 1);\n            const defaultLanguagePathname = pathname.substring(languagePrefix);\n            this.websiteService.goToWebsite({\n                path: defaultLanguagePathname + search + hash,\n                lang: 'default',\n                edition: true\n            });\n        } else {\n            this.websiteContext.edition = true;\n        }\n    }\n}\n\nexport const systrayItem = {\n    Component: EditWebsiteSystray,\n    isDisplayed: (env) => env.services.website.isRestrictedEditor && (env.services.website.currentWebsite.metadata.editable || env.services.website.currentWebsite.metadata.translatable),\n};\n\nregistry.category(\"website_systray\").add(\"EditWebsite\", systrayItem, { sequence: 7 });\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, useState } from \"@odoo/owl\";\n\nclass MobilePreviewSystray extends Component {\n    static template = \"website.MobilePreviewSystray\";\n    static props = {};\n    setup() {\n        this.websiteService = useService('website');\n        this.state = useState(this.websiteService.context);\n    }\n}\n\nexport const systrayItem = {\n    Component: MobilePreviewSystray,\n    isDisplayed: (env) => env.services.website.isRestrictedEditor,\n};\n\nregistry.category(\"website_systray\").add(\"MobilePreview\", systrayItem, { sequence: 11 });\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from '@web/core/registry';\nimport { user } from \"@web/core/user\";\nimport { useService } from '@web/core/utils/hooks';\nimport { redirect } from \"@web/core/utils/urls\";\nimport { WebsiteDialog } from \"@website/components/dialog/dialog\";\nimport { AddPageDialog } from \"@website/components/dialog/add_page_dialog\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { sprintf } from '@web/core/utils/strings';\nimport { Component, xml, useState, onWillStart } from \"@odoo/owl\";\n\nexport const MODULE_STATUS = {\n    NOT_INSTALLED: 'NOT_INSTALLED',\n    INSTALLING: 'INSTALLING',\n    FAILED_TO_INSTALL: 'FAILED_TO_INSTALL',\n    INSTALLED: 'INSTALLED',\n};\n\nclass NewContentElement extends Component {\n    static template = \"website.NewContentElement\";\n    static props = {\n        name: { type: String, optional: true },\n        title: String,\n        onClick: Function,\n        status: { type: String, optional: true },\n        moduleXmlId: { type: String, optional: true },\n        slots: Object,\n    };\n    static defaultProps = {\n        status: MODULE_STATUS.INSTALLED,\n    };\n\n    setup() {\n        this.MODULE_STATUS = MODULE_STATUS;\n    }\n\n    onClick(ev) {\n        ev.preventDefault();\n        ev.stopPropagation();\n        this.props.onClick();\n    }\n}\n\nclass InstallModuleDialog extends Component {\n    static components = { WebsiteDialog };\n    static template = \"website.InstallModuleDialog\";\n    static props = {\n        title: String,\n        installationText: String,\n        installModule: Function,\n        close: Function,\n    };\n\n    setup() {\n        this.installButton = _t(\"Install\");\n    }\n\n    onClickInstall() {\n        this.props.close();\n        this.props.installModule();\n    }\n}\n\nexport class NewContentModal extends Component {\n    static template = \"website.NewContentModal\";\n    static components = { NewContentElement };\n    static props = {};\n\n    setup() {\n        this.orm = useService('orm');\n        this.dialogs = useService('dialog');\n        this.website = useService('website');\n        this.action = useService('action');\n        this.isSystem = user.isSystem;\n\n        this.newContentText = {\n            failed: _t('Failed to install \"%s\"'),\n            installInProgress: _t(\"The installation of an App is already in progress.\"),\n            installNeeded: _t('Do you want to install the \"%s\" App?'),\n            installPleaseWait: _t('Installing \"%s\"'),\n        };\n\n        this.state = useState({\n            newContentElements: [\n                {\n                    moduleName: 'website_blog',\n                    moduleXmlId: 'base.module_website_blog',\n                    status: MODULE_STATUS.NOT_INSTALLED,\n                    icon: xml`<i class=\"fa fa-newspaper-o\"/>`,\n                    title: _t('Blog Post'),\n                },\n                {\n                    moduleName: 'website_event',\n                    moduleXmlId: 'base.module_website_event',\n                    status: MODULE_STATUS.NOT_INSTALLED,\n                    icon: xml`<i class=\"fa fa-ticket\"/>`,\n                    title: _t('Event'),\n                },\n                {\n                    moduleName: 'website_forum',\n                    moduleXmlId: 'base.module_website_forum',\n                    status: MODULE_STATUS.NOT_INSTALLED,\n                    icon: xml`<i class=\"fa fa-comment\"/>`,\n                    redirectUrl: '/forum',\n                    title: _t('Forum'),\n                },\n                {\n                    moduleName: 'website_hr_recruitment',\n                    moduleXmlId: 'base.module_website_hr_recruitment',\n                    status: MODULE_STATUS.NOT_INSTALLED,\n                    icon: xml`<i class=\"fa fa-briefcase\"/>`,\n                    title: _t('Job Position'),\n                },\n                {\n                    moduleName: 'website_sale',\n                    moduleXmlId: 'base.module_website_sale',\n                    status: MODULE_STATUS.NOT_INSTALLED,\n                    icon: xml`<i class=\"fa fa-shopping-cart\"/>`,\n                    title: _t('Product'),\n                },\n                {\n                    moduleName: 'website_slides',\n                    moduleXmlId: 'base.module_website_slides',\n                    status: MODULE_STATUS.NOT_INSTALLED,\n                    icon: xml`<i class=\"fa module_icon\" style=\"background-image: url('/website/static/src/img/apps_thumbs/website_slide.svg');background-repeat: no-repeat; background-position: center;\"/>`,\n                    title: _t('Course'),\n                },\n                {\n                    moduleName: 'website_livechat',\n                    moduleXmlId: 'base.module_website_livechat',\n                    status: MODULE_STATUS.NOT_INSTALLED,\n                    icon: xml`<i class=\"fa fa-comments\"/>`,\n                    title: _t('Livechat Widget'),\n                    redirectUrl: '/livechat'\n                },\n            ]\n        });\n\n        this.websiteContext = useState(this.website.context);\n        useHotkey('escape', () => {\n            if (this.websiteContext.showNewContentModal) {\n                this.websiteContext.showNewContentModal = false;\n            }\n        });\n\n        onWillStart(this.onWillStart.bind(this));\n    }\n\n    async onWillStart() {\n        this.isDesigner = await user.hasGroup('website.group_website_designer');\n        this.canInstall = await user.isAdmin;\n        if (this.canInstall) {\n            const moduleNames = this.state.newContentElements.filter(({status}) => status === MODULE_STATUS.NOT_INSTALLED).map(({moduleName}) => moduleName);\n            this.modulesInfo = {};\n            for (const record of await this.orm.searchRead(\n                \"ir.module.module\",\n                [['name', 'in', moduleNames]],\n                [\"id\", \"name\", \"shortdesc\"],\n            )) {\n                this.modulesInfo[record.name] = {id: record.id, name: record.shortdesc};\n            }\n        }\n        const modelsToCheck = [];\n        const elementsToUpdate = {};\n        for (const element of this.state.newContentElements) {\n            if (element.model) {\n                modelsToCheck.push(element.model);\n                elementsToUpdate[element.model] = element;\n            }\n        }\n        const accesses = await rpc(\"/website/check_new_content_access_rights\", {\n            models: modelsToCheck,\n        });\n        for (const [model, access] of Object.entries(accesses)) {\n            elementsToUpdate[model].isDisplayed = access;\n        }\n    }\n\n    get sortedNewContentElements() {\n        return this.state.newContentElements.filter(({status}) => status !== MODULE_STATUS.NOT_INSTALLED).concat(this.state.newContentElements.filter(({status}) => status === MODULE_STATUS.NOT_INSTALLED));\n    }\n\n    createNewPage() {\n        this.dialogs.add(AddPageDialog, {\n            onAddPage: () => this.websiteContext.showNewContentModal = false,\n            websiteId: this.website.currentWebsite.id,\n        });\n    }\n\n    async installModule(id, redirectUrl) {\n        await this.orm.silent.call(\n            'ir.module.module',\n            'button_immediate_install',\n            [id],\n        );\n        if (redirectUrl) {\n            this.website.prepareOutLoader();\n            window.location.replace(redirectUrl);\n        } else {\n            const { id, metadata: { path, viewXmlid } } = this.website.currentWebsite;\n            const url = new URL(path);\n            if (viewXmlid === 'website.page_404') {\n                url.pathname = '';\n            }\n            // A reload is needed after installing a new module, to instantiate\n            // a NewContentModal with patches from the installed module.\n            this.website.prepareOutLoader();\n            redirect(`/odoo/action-website.website_preview?website_id=${id}&path=${encodeURIComponent(url.toString())}&display_new_content=true`);\n        }\n    }\n\n    onClickNewContent(element) {\n        if (element.createNewContent) {\n            return element.createNewContent();\n        }\n\n        const {id, name} = this.modulesInfo[element.moduleName];\n        const dialogProps = {\n            title: element.title,\n            installationText: sprintf(this.newContentText.installNeeded, name),\n            installModule: async () => {\n                // Update the NewContentElement with installing icon and text.\n                this.state.newContentElements = this.state.newContentElements.map(el => {\n                    if (el.moduleXmlId === element.moduleXmlId) {\n                        el.status = MODULE_STATUS.INSTALLING;\n                        el.icon = xml`<i class=\"fa fa-spin fa-circle-o-notch\"/>`;\n                        el.title = sprintf(this.newContentText.installPleaseWait, name);\n                    }\n                    return el;\n                });\n                this.website.showLoader({ title: _t(\"Building your %s\", name) });\n                try {\n                    await this.installModule(id, element.redirectUrl);\n                } catch (error) {\n                    this.website.hideLoader();\n                    // Update the NewContentElement with failure icon and text.\n                    this.state.newContentElements = this.state.newContentElements.map(el => {\n                        if (el.moduleXmlId === element.moduleXmlId) {\n                            el.status = MODULE_STATUS.FAILED_TO_INSTALL;\n                            el.icon = xml`<i class=\"fa fa-exclamation-triangle\"/>`;\n                            el.title = sprintf(this.newContentText.failed, name);\n                        }\n                        return el;\n                    });\n                    console.error(error);\n                }\n            },\n        };\n        this.dialogs.add(InstallModuleDialog, dialogProps);\n    }\n\n    /**\n     * This method registers the action to perform when a new content is\n     * saved. The path must be computed once the record is saved, to\n     * perform the 'ir.act_window_close' action, which will be used when\n     * the dialog is closed to go to the correct website page.\n     */\n    async onAddContent(action, edition = false, context = null) {\n        this.action.doAction(action, {\n            additionalContext: (context) ? context: {},\n            onClose: (infos) => {\n                if (infos) {\n                    this.website.goToWebsite({ path: infos.path, edition: edition });\n                }\n            },\n            props: {\n                onSave: (record, params) => {\n                    if (record.resId) {\n                        const path = params.computePath();\n                        this.action.doAction({\n                            type: \"ir.actions.act_window_close\",\n                            infos: { path }\n                        });\n                    }\n                }\n            }\n        });\n    }\n}\n\nclass NewContentSystray extends Component {\n    static template = \"website.NewContentSystray\";\n    static components = { NewContentModal };\n    static props = {};\n\n    setup() {\n        this.website = useService('website');\n        this.websiteContext = useState(this.website.context);\n    }\n\n    onClick() {\n        this.websiteContext.showNewContentModal = !this.websiteContext.showNewContentModal;\n    }\n}\n\nexport const systrayItem = {\n    Component: NewContentSystray,\n    isDisplayed: (env) => env.services.website.isRestrictedEditor,\n};\n\nregistry.category(\"website_systray\").add(\"NewContent\", systrayItem, { sequence: 9 });\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { CheckBox } from '@web/core/checkbox/checkbox';\nimport { useService, useBus } from '@web/core/utils/hooks';\nimport { Component, xml, useState } from \"@odoo/owl\";\nimport { OptimizeSEODialog } from \"@website/components/dialog/seo\";\nimport { checkAndNotifySEO } from \"@website/js/utils\";\n\nconst websiteSystrayRegistry = registry.category('website_systray');\n\nclass PublishSystray extends Component {\n    static template = xml`\n        <div t-on-click=\"publishContent\" class=\"o_menu_systray_item o_website_publish_container d-flex ms-auto\" t-att-data-processing=\"state.processing and 1\">\n            <a href=\"#\" class=\"d-flex align-items-center mx-1 px-2 px-md-0\" data-hotkey=\"p\">\n                <span class=\"o_nav_entry d-none d-md-block mx-0 pe-1\" t-esc=\"this.label\"/>\n                <CheckBox value=\"state.published\" className=\"'form-switch d-flex justify-content-center m-0 pe-none'\"/>\n            </a>\n        </div>`;\n    static components = {\n        CheckBox,\n    };\n    static props = {};\n\n    setup() {\n        this.website = useService('website');\n        this.orm = useService('orm');\n        this.dialogService = useService(\"dialog\");\n        this.notificationService = useService(\"notification\");\n\n        this.state = useState({\n            published: this.website.currentWebsite.metadata.isPublished,\n            processing: false,\n        });\n\n        useBus(websiteSystrayRegistry, 'CONTENT-UPDATED', () => this.state.published = this.website.currentWebsite.metadata.isPublished);\n    }\n\n    get label() {\n        return this.state.published ? _t(\"Published\") : _t(\"Unpublished\");\n    }\n\n    /**\n     * @todo event handlers should probably never return a Promise using OWL,\n     * to adapt in master.\n     */\n    async publishContent() {\n        if (this.state.processing) {\n            return;\n        }\n        this.state.processing = true;\n        this.state.published = !this.state.published;\n        const { metadata: { mainObject } } = this.website.currentWebsite;\n        return this.orm.call(\n            mainObject.model,\n            \"website_publish_button\",\n            [[mainObject.id]],\n        ).then(\n            async (published) => {\n                this.state.published = published;\n                if (published && this.website.currentWebsite.metadata.canOptimizeSeo) {\n                    const seo_data = await rpc(\"/website/get_seo_data\", {\n                        res_id: mainObject.id,\n                        res_model: mainObject.model,\n                    });\n                    checkAndNotifySEO(seo_data, OptimizeSEODialog, {\n                        notification: this.notificationService,\n                        dialog: this.dialogService,\n                    });\n                }\n                this.state.processing = false;\n                return published;\n            },\n            (err) => {\n                this.state.published = !this.state.published;\n                this.state.processing = false;\n                throw err;\n            }\n        );\n    }\n}\n\nexport const systrayItem = {\n    Component: PublishSystray,\n    isDisplayed: env => env.services.website.currentWebsite && env.services.website.currentWebsite.metadata.canPublish,\n};\n\nwebsiteSystrayRegistry.add(\"Publish\", systrayItem, { sequence: 12 });\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { session } from \"@web/session\";\nimport wUtils from '@website/js/utils';\nimport { Component } from \"@odoo/owl\";\n\nexport class WebsiteSwitcherSystray extends Component {\n    static template = \"website.WebsiteSwitcherSystray\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n    };\n    static props = {};\n    setup() {\n        this.websiteService = useService('website');\n        this.notificationService = useService(\"notification\");\n        this.actionService = useService(\"action\");\n    }\n\n    getElements() {\n        return this.websiteService.websites.map((website) => ({\n            name: website.name,\n            id: website.id,\n            domain: website.domain,\n            dataset: Object.assign({\n                'data-website-id': website.id,\n            }, website.domain ? {} : {\n                'data-tooltip': _t('This website does not have a domain configured.'),\n                'data-tooltip-position': 'left',\n            }),\n            callback: () => {\n                // TODO share this condition with the website_preview somehow\n                // -> we should probably show the redirection warning here too\n                if (!session.website_bypass_domain_redirect // Used by the Odoo support (bugs to be expected)\n                        && website.domain\n                        && !wUtils.isHTTPSorNakedDomainRedirection(website.domain, window.location.origin)) {\n                    const { location: { pathname, search, hash } } = this.websiteService.contentWindow;\n                    const path = pathname + search + hash;\n                    window.location.href = `${encodeURI(website.domain)}/odoo/action-website.website_preview?path=${encodeURIComponent(path)}&website_id=${encodeURIComponent(website.id)}`;\n                } else {\n                    this.websiteService.goToWebsite({ websiteId: website.id, path: \"\", lang: \"default\" });\n                    if (!website.domain) {\n                        const closeFn = this.notificationService.add(\n                            _t(\n                                \"This website does not have a domain configured. To avoid unexpected behaviours during website edition, we recommend closing (or refreshing) other browser tabs.\\nTo remove this message please set a domain in your website settings\"\n                            ),\n                            {\n                                type: \"warning\",\n                                title: _t(\n                                    \"No website domain configured for this website.\"\n                                ),\n                                sticky: true,\n                                buttons: [\n                                    {\n                                        onClick: () => {\n                                            this.actionService.doAction(\n                                                \"website.action_website_configuration\"\n                                            );\n                                            closeFn();\n                                        },\n                                        primary: true,\n                                        name: \"Go to Settings\",\n                                    },\n                                ],\n                            }\n                        );\n                        browser.setTimeout(closeFn, 7000);\n                    }\n                }\n            },\n            class: website.id === this.websiteService.currentWebsite.id ? 'text-truncate active' : 'text-truncate',\n        }));\n    }\n}\n\nexport const systrayItem = {\n    Component: WebsiteSwitcherSystray,\n    isDisplayed: env => env.services.website.hasMultiWebsites,\n};\n\nregistry.category(\"website_systray\").add(\"WebsiteSwitcher\", systrayItem, { sequence: 12 });\n", "/** @odoo-module **/\n\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { Component, useRef, useState } from \"@odoo/owl\";\n\nexport class HierarchyNavbar extends Component {\n    static template = \"website.hierarchy_navbar\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n    };\n    static props = {\n        toggleInactive: Function,\n        websites: Object,\n        selectWebsite: Function,\n        searchView: Function,\n    };\n\n    setup() {\n        this.searchInput = useRef(\"search\");\n        this.websiteNamesState = useState(Array.from(this.props.websites.names));\n    }\n\n    get websiteNames() {\n        return this.websiteNamesState.map((websiteName) => ({\n            label: websiteName,\n            onSelected: () => this.props.selectWebsite(websiteName),\n        }));\n    }\n\n    /**\n     * @param {Event} event\n     */\n    onInputKeydown(event) {\n        if (event.key === \"Enter\" || event.key === \"Tab\") {\n            event.preventDefault();\n            this.props.searchView(event.target.value, !event.shiftKey);\n        }\n    }\n\n    /**\n     * @param {Event} event\n     */\n    onInputClick(event) {\n        this.props.searchView(this.searchInput.el.value, !event.shiftKey);\n    }\n}\n", "/** @odoo-module **/\n\nimport { HierarchyNavbar } from \"./hierarchy_navbar\";\nimport { Layout } from \"@web/search/layout\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, onWillStart, useEffect, useState } from \"@odoo/owl\";\nimport { router } from \"@web/core/browser/router\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\n\nexport class ViewHierarchy extends Component {\n    static components = { Layout, HierarchyNavbar };\n    static template = \"website.view_hierarchy\";\n    static props = { ...standardActionServiceProps };\n    setup() {\n        this.action = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.state = useState({ showInactive: false, searchedView: {}, viewTree: {} });\n        this.websites = useState({ names: new Set([\"All Websites\"]), selected: \"All Websites\" });\n        this.viewId = this.props.action.context.active_id || router.current.active_id;\n        this.hideGenericViewByWebsite = {};\n\n        onWillStart(async () => {\n            ({\n                sibling_views: this.siblingViews,\n                hierarchy: this.state.viewTree,\n            } = await this.orm.call(\"ir.ui.view\", \"get_view_hierarchy\", [this.viewId], {}));\n\n            this.setupWebsiteNames();\n            this.setupHideGenericViewByWebsite();\n            this.linkViewsToParent();\n        });\n\n        useEffect(\n            (searchFoundElem) => {\n                if (searchFoundElem) {\n                    searchFoundElem.scrollIntoView({ behavior: \"smooth\", block: \"center\" });\n                }\n            },\n            () => [document.querySelector(\".o_search_found\")]\n        );\n    }\n\n    /**\n     * Filter the treeView by website\n     * @param {String} websiteName\n     */\n    selectWebsite(websiteName) {\n        this.websites.selected = websiteName;\n    }\n\n    /**\n     * Show/hide inactive views\n     * @param {Boolean} checked\n     */\n    toggleInactive(checked) {\n        this.state.showInactive = checked;\n    }\n\n    /**\n     * @param {String} keyword\n     * @returns {Array} a list of visible views that match the keyword\n     * insensitive case.\n     * The comparison is done on the name, the key and the id of each views.\n     * Priority is given to the exact matches and then to the order\n     */\n    getSearchResults(keyword) {\n        const exactMatches = [];\n        const matches = [];\n        const lowercaseKeyword = keyword.toLowerCase();\n        this.viewTraversal(\n            this.state.viewTree,\n            (currentView) => {\n                if (\n                    this.isViewDisplayed(currentView) &&\n                    (currentView.name.toLowerCase() === lowercaseKeyword ||\n                        currentView.key.toLowerCase() === lowercaseKeyword ||\n                        currentView.id === parseInt(lowercaseKeyword))\n                ) {\n                    exactMatches.push(currentView);\n                } else if (\n                    this.isViewDisplayed(currentView) &&\n                    (currentView.name.toLowerCase().includes(lowercaseKeyword) ||\n                        currentView.key.toLowerCase().includes(lowercaseKeyword))\n                ) {\n                    matches.push(currentView);\n                }\n            },\n            (currentView) => this.isViewDisplayed(currentView)\n        );\n        return exactMatches.concat(matches);\n    }\n\n    /**\n     * Search the next visibile view that matches the keyword to the name, the\n     * key and the id of the view\n     * @param {String} keyword\n     * @param {Boolean} forward\n     */\n    searchView(keyword, forward = true) {\n        const matches = this.getSearchResults(keyword);\n        let index = 0;\n        if (this.state.searchedView.keyword === keyword) {\n            index = matches.findIndex((view) => this.state.searchedView.id === view.id);\n            index = forward ? index + 1 : index - 1;\n            index = index - matches.length * Math.floor(index / matches.length);\n        }\n\n        const view = matches[index];\n        if (view) {\n            this.state.searchedView = {\n                id: view.id,\n                keyword: keyword,\n                total: matches.length,\n                index,\n            };\n        }\n    }\n\n    /**\n     * Makes an inorder traversal of the view tree and apply a function at each\n     * node\n     * @param {Object} currentView represent the current view tree\n     * @param {Function} fn function applied at each node with currentView as\n     * parameter\n     * @param {Function} continueRec take the view as argument and decide if\n     * the recursion continue\n     */\n    viewTraversal(currentView, fn, continueRec = (view) => true) {\n        fn(currentView);\n        if (continueRec(currentView)) {\n            currentView.inherit_children.forEach((childView) => {\n                this.viewTraversal(childView, fn, continueRec);\n            });\n        }\n    }\n\n    /**\n     * Setup website names from the viewTree into this.websites.names\n     */\n    setupWebsiteNames() {\n        this.viewTraversal(this.state.viewTree, (currentView) => {\n            if (currentView.website_name) {\n                this.websites.names.add(currentView.website_name);\n            }\n        });\n    }\n\n    /**\n     * States for each website filter if a generic view should be hided or not\n     */\n    setupHideGenericViewByWebsite() {\n        this.viewTraversal(this.state.viewTree, (currentView) => {\n            if (currentView.website_name) {\n                if (!this.hideGenericViewByWebsite[currentView.website_name]) {\n                    this.hideGenericViewByWebsite[currentView.website_name] = {};\n                }\n                this.hideGenericViewByWebsite[currentView.website_name][currentView.name] = true;\n            }\n        });\n    }\n\n    /**\n     * Link views in the viewTree to their parent\n     */\n    linkViewsToParent() {\n        this.viewTraversal(this.state.viewTree, (currentView) => {\n            currentView.inherit_children.forEach((child) => (child.parent = currentView));\n        });\n    }\n\n    /**\n     * Collapse the view to show/hide the children\n     * @param {Object} view\n     */\n    onCollapseClick(view) {\n        view.collapsed = !view.collapsed;\n        if (view.collapsed) {\n            // When folding a parent, children should also fold\n            this.viewTraversal(view, (child) => {\n                child.collapsed = view.collapsed;\n            });\n        }\n    }\n\n    /**\n     * @param {Object} view\n     * @param {Boolean} isCollapsedDisplayed\n     * @returns true if the view is displayed in the view tree, false otherwise\n     */\n    isViewDisplayed(view, isCollapsedDisplayed = false) {\n        let isCollapsed = view.parent ? view.parent.collapsed : false;\n        if (isCollapsedDisplayed) {\n            isCollapsed = false;\n        }\n        const isActive = this.state.showInactive || view.active;\n        const isWebsiteDisplayed =\n            this.websites.selected === \"All Websites\" ||\n            view.website_name === this.websites.selected ||\n            (!view.website_name &&\n                !this.hideGenericViewByWebsite[this.websites.selected][view.name]);\n        return !isCollapsed && isActive && isWebsiteDisplayed;\n    }\n\n    /**\n     * @param {Object} view\n     * @returns true if view has a child to unfold, false otherwise\n     */\n    hasChildToUnfold(view) {\n        return view.inherit_children.some((child) => this.isViewDisplayed(child, true));\n    }\n\n    /**\n     * @param {Number} viewId\n     */\n    onShowDiffClick(viewId) {\n        this.action.doAction(\"base.reset_view_arch_wizard_action\", {\n            additionalContext: {\n                active_model: \"ir.ui.view\",\n                active_ids: [viewId],\n            },\n        });\n    }\n\n    /**\n     * @param {Number} viewId\n     */\n    openFormView(viewId) {\n        this.action.doAction({\n            type: \"ir.actions.act_window\",\n            res_model: \"ir.ui.view\",\n            res_id: viewId,\n            views: [[false, \"form\"]],\n        });\n    }\n\n    /**\n     * @param {Number} viewId\n     */\n    onShowHierarchy(viewId) {\n        this.action.doAction({\n            type: \"ir.actions.client\",\n            tag: \"website_view_hierarchy\",\n            name: \"View Hierarchy\",\n            context: {\n                active_id: viewId,\n            },\n        });\n    }\n}\n\nregistry.category(\"actions\").add(\"website_view_hierarchy\", ViewHierarchy);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { MenuDialog } from \"@website/components/dialog/edit_menu\";\n\n/**\n * The goal of this patch is to prevent users from creating\n * website menu item with url of format /helpdesk/<team-slug>\n */\npatch(MenuDialog.prototype, {\n    /**\n     * @override\n     */\n    setup() {\n        super.setup();\n        this.notification = useService('notification');\n    },\n    /**\n     * @override\n     */\n    onClickOk() {\n        if (this.url.isValid()) {\n            const helpdesk_team_pattern = /^\\/helpdesk\\/([a-zA-Z]+-)+\\d+$/;\n            if (helpdesk_team_pattern.test(this.url.input.value)) {\n                this.url.input.hasError = true;\n                this.notification.add(\n                    _t(\"The %s URL is reserved for the helpdesk team with the same name. \\\n                        To use it, please enable the 'website form' feature on that team instead.\", this.url.input.value),\n                    { type: 'danger' },\n                );\n                return;\n            }\n        }\n        super.onClickOk();\n    },\n});\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { NewContentModal, MODULE_STATUS } from '@website/systray_items/new_content';\nimport { patch } from \"@web/core/utils/patch\";\nimport { xml } from \"@odoo/owl\";\n\npatch(NewContentModal.prototype, {\n    setup() {\n        super.setup();\n\n        this.state.newContentElements.push({\n            moduleName: 'website_appointment',\n            moduleXmlId: 'base.module_website_appointment',\n            status: MODULE_STATUS.NOT_INSTALLED,\n            icon: xml`<i class=\"fa fa-calendar\"/>`,\n            title: _t('Appointment Form'),\n        });\n    },\n});\n", "import { browser } from \"@web/core/browser/browser\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { colorSchemeService } from \"@web_enterprise/webclient/color_scheme/color_scheme_service\";\n\npatch(colorSchemeService, {\n    reload() {\n        if (document.querySelector(\"header.o_navbar + .o_action_manager > .o_website_preview\")) {\n            browser.location.pathname = \"/@\" + browser.location.pathname;\n        } else {\n            super.reload();\n        }\n    },\n});\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { cookie } from \"@web/core/browser/cookie\";\n\nimport { markup } from \"@odoo/owl\";\nimport { omit } from \"@web/core/utils/objects\";\nimport { waitForStable } from \"@web/core/macro\";\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        async run() {\n            await new Promise((resolve) => {\n                let elapsedTime = 0;\n                const intervalTime = 100;\n                const interval = setInterval(() => {\n                    if (window.location.pathname.startsWith(pathname)) {\n                        clearInterval(interval);\n                        resolve();\n                    }\n                    elapsedTime += intervalTime;\n                    if (elapsedTime >= 5000) {\n                        clearInterval(interval);\n                        console.error(`The pathname ${pathname} has not been found`);\n                    }\n                }, intervalTime);\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: \"body:not(.editor_has_snippets) .o_menu_systray .o_edit_website_container a\",\n        tooltipPosition: position,\n        run: \"click\",\n    }, {\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        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 = 50000) {\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: \"button[data-action=save]:enabled:contains(save)\",\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            async run(actions) {\n                await waitForStable(document, 1000);\n                await actions.click();\n            },\n            timeout,\n        },\n        {\n            trigger:\n                \"body:not(.editor_has_dummy_snippets):not(.o_website_navbar_hide):not(.editor_has_snippets):not(:has(.o_notification_bar))\",\n            noPrepend: true,\n            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        {\n            content: \"Click on the extra menu dropdown toggle if it is there and not shown\",\n            trigger: `${\n                backend ? \":iframe\" : \"\"\n            } ul.top_menu`,\n            run(actions) {\n                // Note: the button might not exist (it only appear if there is many menu items)\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                    actions.click(extraMenuButton);\n                }\n            },\n        },\n        stepOptions\n    );\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    registry.category(\"web_tour.tours\").remove(name);\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                    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(\n        \"homepage\",\n        {\n            url: \"/\",\n        },\n        () => [\n            ...clickOnEditAndWaitEditMode(),\n            ...prepend_trigger(\n                steps().concat(clickOnSave()),\n                \".o_website_preview[data-view-xmlid='website.homepage'] \"\n            ),\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[data-website-id=\"${websiteId}\"]: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* Switches to a different website by clicking on the website switcher.\n* This function can only be used during test tours as it requires\n* specific cookies to properly function.\n*\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 testSwitchWebsite(websiteName) {\n   const websiteIdMapping = JSON.parse(cookie.get('websiteIdMapping') || '{}');\n   const websiteId = websiteIdMapping[websiteName];\n   return switchWebsite(websiteId, websiteName)\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 { isVisible } from \"@web/core/utils/ui\";\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    const endHighlightUpdate = () =>\n        topTextEl.dispatchEvent(new Event(\"text_highlight_added\", { bubbles: true }));\n    // Don't reapply the effects to a highlighted text.\n    // If the target is invisible, we still need to notify the public widget\n    // that a highlight was detected (It's needed anyway, so the public widget\n    // can link the element to its observer, which tracks size changes and\n    // adapts the highlights accordingly).\n    if (topTextEl.querySelector(\".o_text_highlight_item\") || !isVisible(topTextEl)) {\n        return endHighlightUpdate();\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    endHighlightUpdate();\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    if (!isVisible(textEl)) {\n        // No need to adapt the effects on hidden targets, since they will be\n        // immediately fixed by the `resizeObserver` once they become visible.\n        // This will also prevent conflicts with the field's synchronizations\n        // in some specific cases (e.g. desktop & mobile navbar duplicated\n        // fields with highlighted content).\n        return;\n    }\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", "/** @odoo-module **/\n\nimport { browser } from \"@web/core/browser/browser\";\nconst sessionStorage = browser.sessionStorage;\nimport { AutoComplete } from \"@web/core/autocomplete/autocomplete\";\nimport { delay } from \"@web/core/utils/concurrency\";\nimport { getDataURLFromFile, redirect } from \"@web/core/utils/urls\";\nimport weUtils from '@web_editor/js/common/utils';\nimport { _t } from \"@web/core/l10n/translation\";\nimport { svgToPNG, webpToPNG } from \"@website/js/utils\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { mixCssColors } from '@web/core/utils/colors';\nimport { router } from \"@web/core/browser/router\";\nimport {\n    Component,\n    onMounted,\n    reactive,\n    useEffect,\n    useEnv,\n    useRef,\n    useState,\n    useSubEnv,\n    onWillStart,\n    useExternalListener,\n} from \"@odoo/owl\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\nimport { addLoadingEffect as addButtonLoadingEffect } from \"@web/core/utils/ui\";\n\nexport const ROUTES = {\n    descriptionScreen: 2,\n    paletteSelectionScreen: 3,\n    featuresSelectionScreen: 4,\n    themeSelectionScreen: 5,\n};\n\nexport const WEBSITE_TYPES = {\n    1: {id: 1, label: _t(\"a business website\"), name: 'business'},\n    2: {id: 2, label: _t(\"an online store\"), name: 'online_store'},\n    3: {id: 3, label: _t(\"a blog\"), name: 'blog'},\n    4: {id: 4, label: _t(\"an event website\"), name: 'event'},\n    5: {id: 5, label: _t(\"an elearning platform\"), name: 'elearning'},\n};\n\nexport const WEBSITE_PURPOSES = {\n    1: {id: 1, label: _t(\"get leads\"), name: 'get_leads'},\n    2: {id: 2, label: _t(\"develop the brand\"), name: 'develop_brand'},\n    3: {id: 3, label: _t(\"sell more\"), name: 'sell_more'},\n    4: {id: 4, label: _t(\"inform customers\"), name: 'inform_customers'},\n    5: {id: 5, label: _t(\"schedule appointments\"), name: 'schedule_appointments'},\n};\n\nexport const PALETTE_NAMES = [\n    'default-light-1',\n    'default-light-2',\n    'default-light-4',\n    'default-light-3',\n    'default-light-5',\n    'default-24',\n    'default-light-7',\n    'default-light-6',\n    'default-light-11',\n    'default-light-14',\n    'default-light-8',\n    'default-6',\n    'default-7',\n    'default-8',\n    'default-9',\n    'default-23',\n    'default-25',\n    'default-12',\n    'default-14',\n    'default-22',\n    'default-15',\n    'default-16',\n    'default-17',\n    'default-light-10',\n    'default-19',\n    'default-20',\n    'default-5',\n    'default-4',\n    'default-light-9',\n    'default-2',\n    'default-light-13',\n    'default-27',\n    'default-light-12',\n    'default-1',\n    'default-28',\n    'default-21',\n];\n\n// Attributes for which background color should be retrieved\n// from CSS and added in each palette.\nexport const CUSTOM_BG_COLOR_ATTRS = [\"menu\", \"footer\"];\n\nconst MAX_NBR_DISPLAY_MAIN_THEMES = 3;\n\n/**\n * Returns a list of maximum \"resultNbrMax\" themes that depends on the wanted\n * industry and the color palette.\n *\n * @param {Object} orm - The orm used for the server call.\n * @param {Object} state - The state that contains the wanted industry and color\n * palette.\n * @param {Number} resultNbrMax - The number of different wanted themes.\n * @returns {Promise<Array>} A list of objects that contains the different\n * theme names and their related text svgs (as result of a Promise). The length\n * of the list is at most 'resultNbrMax'.\n */\nasync function getRecommendedThemes(orm, state, resultNbrMax = MAX_NBR_DISPLAY_MAIN_THEMES) {\n    return orm.call(\"website\",\n        \"configurator_recommended_themes\",\n        [],\n        {\n            \"industry_id\": state.selectedIndustry.id,\n            \"palette\": state.selectedPalette,\n            \"result_nbr_max\": resultNbrMax,\n        },\n    );\n}\n\n//------------------------------------------------------------------------------\n// Components\n//------------------------------------------------------------------------------\n\nexport class SkipButton extends Component {\n    static template = \"website.Configurator.SkipButton\";\n    static props = {\n        skip: Function,\n    };\n}\n\nexport class WelcomeScreen extends Component {\n    static template = \"website.Configurator.WelcomeScreen\";\n    static components = { SkipButton };\n    static props = {\n        skip: Function,\n        navigate: Function,\n    };\n    setup() {\n        this.state = useStore();\n    }\n\n    goToDescription() {\n        this.props.navigate(ROUTES.descriptionScreen);\n    }\n}\n\nexport class IndustrySelectionAutoComplete extends AutoComplete {\n    static timeout = 400;\n\n    get dropdownOptions() {\n        return {\n            ...super.dropdownOptions,\n            position: \"bottom-fit\",\n        };\n    }\n\n    get ulDropdownClass() {\n        return `${super.ulDropdownClass} custom-ui-autocomplete shadow-lg border-0 o_configurator_show_fast o_configurator_industry_dropdown`;\n    }\n}\n\nexport class DescriptionScreen extends Component {\n    static template = 'website.Configurator.DescriptionScreen';\n    static components = { SkipButton, AutoComplete: IndustrySelectionAutoComplete };\n    static props = {\n        navigate: Function,\n        skip: Function,\n    };\n    setup() {\n        this.industrySelection = useRef('industrySelection');\n        this.state = useStore();\n        this.orm = useService('orm');\n\n        onMounted(() => this.onMounted());\n    }\n\n    onMounted() {\n        this.selectWebsitePurpose();\n    }\n    /**\n     * Set the input's parent label value to automatically adapt input size\n     * and update the selected industry.\n     *\n     * @private\n     * @param {Object} suggestion an industry\n     */\n    _setSelectedIndustry(suggestion) {\n        const { label, id } = Object.getPrototypeOf(suggestion);\n        this.state.selectIndustry(label, id);\n        this.checkDescriptionCompletion();\n    }\n\n    get sources() {\n        return [\n            {\n                options: (request) => {\n                    return request.length < 1 ? [] : this._autocompleteSearch(request);\n                },\n            },\n        ];\n    }\n    /**\n     * Called each time the autocomplete input's value changes. Only industries\n     * having a label or a synonym containing all terms of the input value are\n     * kept.\n     * The order received from IAP is kept (expected to be on descending hit\n     * count) unless there are 7 or less matches in which case the results are\n     * sorted alphabetically.\n     * The result size is limited to 30.\n     *\n     * @param {String} term input current value\n     */\n    _autocompleteSearch(term) {\n        const terms = term.toLowerCase().split(/[|,\\n]+/);\n        const limit = 30;\n        const sortLimit = 7;\n        // `this.state.industries` is already sorted by hit count (from IAP).\n        // That order should be kept after manipulating the recordset.\n        let matches = this.state.industries.filter((val, index) => {\n            // To match, every term should be contained in either the label or a\n            // synonym\n            for (const candidate of [val.label, ...(val.synonyms || '').split(/[|,\\n]+/)]) {\n                if (terms.every(term => candidate.toLowerCase().includes(term))) {\n                    return true;\n                }\n            }\n        });\n        if (matches.length > limit) {\n            // Keep matches with the least number of words so that e.g.\n            // \"restaurant\" remains available even if there are 30 specific\n            // sub-types that have a higher hit count.\n            matches = matches.sort((x, y) => x.wordCount - y.wordCount)\n                             .slice(0, limit)\n                             .sort((x, y) => x.hitCountOrder - y.hitCountOrder);\n        }\n        if (matches.length <= sortLimit) {\n            // Sort results by ascending label if few of them.\n            matches = matches.sort((x, y) => (x.label < y.label ? -1 : x.label > y.label ? 1 : 0));\n        }\n        return matches.length ? matches : [{ label: term, id: -1 }];\n    }\n\n    selectWebsiteType(id) {\n        this.state.selectWebsiteType(id);\n        setTimeout(() => {\n            this.industrySelection.el.querySelector(\"input\").focus();\n        });\n        this.checkDescriptionCompletion();\n    }\n\n    selectWebsitePurpose(id) {\n        this.state.selectWebsitePurpose(id);\n        this.checkDescriptionCompletion();\n    }\n\n    checkDescriptionCompletion() {\n        const {selectedType, selectedPurpose, selectedIndustry} = this.state;\n        if (selectedType && selectedPurpose && selectedIndustry) {\n            // If the industry name is not known by the server, send it to the\n            // IAP server.\n            if (selectedIndustry.id === -1) {\n                this.orm.call('website', 'configurator_missing_industry', [], {\n                    'unknown_industry': selectedIndustry.label,\n                });\n            }\n            this.props.navigate(ROUTES.paletteSelectionScreen);\n        }\n    }\n}\n\nexport class PaletteSelectionScreen extends Component {\n    static components = {SkipButton};\n    static template = 'website.Configurator.PaletteSelectionScreen';\n    static props = {\n        navigate: Function,\n        skip: Function,\n    };\n    setup() {\n        this.state = useStore();\n        this.logoInputRef = useRef('logoSelectionInput');\n        this.notification = useService(\"notification\");\n        this.orm = useService('orm');\n\n        onMounted(() => {\n            if (this.state.logo) {\n                this.updatePalettes();\n            }\n        });\n    }\n\n    uploadLogo() {\n        this.logoInputRef.el.click();\n    }\n\n    /**\n     * Removes the previously uploaded logo.\n     *\n     * @param {Event} ev\n     */\n    async removeLogo(ev) {\n        ev.stopPropagation();\n        // Permit to trigger onChange even with the same file.\n        this.logoInputRef.el.value = \"\";\n        if (this.state.logoAttachmentId) {\n            await this._removeAttachments([this.state.logoAttachmentId]);\n        }\n        this.state.changeLogo();\n        // Remove recommended palette.\n        this.state.setRecommendedPalette();\n    }\n\n    async changeLogo() {\n        const logoSelectInput = this.logoInputRef.el;\n        if (logoSelectInput.files.length === 1) {\n            const previousLogoAttachmentId = this.state.logoAttachmentId;\n            const file = logoSelectInput.files[0];\n            const data = await getDataURLFromFile(file);\n            const attachment = await rpc('/web_editor/attachment/add_data', {\n                'name': 'logo',\n                'data': data.split(',')[1],\n                'is_image': true,\n            });\n            if (!attachment.error) {\n                if (previousLogoAttachmentId) {\n                    await this._removeAttachments([previousLogoAttachmentId]);\n                }\n                this.state.changeLogo(data, attachment.id);\n                this.updatePalettes();\n            } else {\n                this.notification.add(\n                    attachment.error,\n                    {\n                        title: file.name,\n                    }\n                );\n            }\n        }\n    }\n\n    async updatePalettes() {\n        let img = this.state.logo;\n        if (img.startsWith('data:image/svg+xml')) {\n            img = await svgToPNG(img);\n        }\n        if (img.startsWith('data:image/webp')) {\n            img = await webpToPNG(img);\n        }\n        img = img.split(',')[1];\n        const [color1, color2] = await this.orm.call('base.document.layout',\n            'extract_image_primary_secondary_colors',\n            [img],\n            {mitigate: 255},\n        );\n        this.state.setRecommendedPalette(color1, color2);\n    }\n\n    selectPalette(paletteName) {\n        this.state.selectPalette(paletteName);\n        this.props.navigate(ROUTES.featuresSelectionScreen);\n    }\n\n    /**\n     * Removes the attachments from the DB.\n     *\n     * @private\n     * @param {Array<number>} ids the attachment ids to remove\n     */\n    async _removeAttachments(ids) {\n        rpc(\"/web_editor/attachment/remove\", { ids: ids });\n    }\n}\n\nexport class ApplyConfiguratorScreen extends Component {\n    static template = \"\";\n    static props = [\"*\"];\n    setup() {\n        this.websiteService = useService('website');\n    }\n\n    async applyConfigurator(themeName) {\n        if (!this.state.selectedIndustry) {\n            return this.props.navigate(ROUTES.descriptionScreen);\n        }\n        if (!this.state.selectedPalette) {\n            return this.props.navigate(ROUTES.paletteSelectionScreen);\n        }\n\n        const attemptConfiguratorApply = async (data, retryCount = 0) => {\n            try {\n                return await this.orm.silent.call('website',\n                    'configurator_apply', [], data\n                );\n            } catch (error) {\n                // Wait a bit before retrying or allowing manual retry.\n                await delay(5000);\n                if (retryCount < 3) {\n                    return attemptConfiguratorApply(data, retryCount + 1);\n                }\n                document.querySelector('.o_website_loader_container').remove();\n                throw error;\n            }\n        };\n\n        if (themeName !== undefined) {\n            const selectedFeatures = Object.values(this.state.features).filter((feature) => feature.selected).map((feature) => feature.id);\n            this.websiteService.showLoader({\n                showTips: true,\n                selectedFeatures: selectedFeatures,\n                showWaitingMessages: true,\n            });\n            let selectedPalette = this.state.selectedPalette.name;\n            if (!selectedPalette) {\n                selectedPalette = [\n                    this.state.selectedPalette.color1,\n                    this.state.selectedPalette.color2,\n                    this.state.selectedPalette.color3,\n                    this.state.selectedPalette.color4,\n                    this.state.selectedPalette.color5,\n                ];\n            }\n\n            const data = {\n                'selected_features': selectedFeatures,\n                'industry_id': this.state.selectedIndustry.id,\n                'industry_name': this.state.selectedIndustry.label.toLowerCase(),\n                'selected_palette': selectedPalette,\n                'theme_name': themeName,\n                'website_purpose': WEBSITE_PURPOSES[\n                    this.state.selectedPurpose || this.state.formerSelectedPurpose\n                ].name,\n                'website_type': WEBSITE_TYPES[this.state.selectedType].name,\n                'logo_attachment_id': this.state.logoAttachmentId,\n            };\n            const resp = await attemptConfiguratorApply(data);\n\n            this.props.clearStorage();\n\n            this.websiteService.prepareOutLoader();\n            // Here the website service goToWebsite method is not used because\n            // the web client needs to be reloaded after the new modules have\n            // been installed.\n            redirect(`/odoo/action-website.website_preview?website_id=${encodeURIComponent(resp.website_id)}`);\n        }\n    }\n}\n\nexport class FeaturesSelectionScreen extends ApplyConfiguratorScreen {\n    static components = {SkipButton};\n    static template = 'website.Configurator.FeatureSelection';\n    setup() {\n        super.setup();\n\n        this.orm = useService(\"orm\");\n        this.state = useStore();\n    }\n\n    async buildWebsite() {\n        const industryId = this.state.selectedIndustry && this.state.selectedIndustry.id;\n        if (!industryId) {\n            return this.props.navigate(ROUTES.descriptionScreen);\n        }\n        const themes = await getRecommendedThemes(this.orm, this.state);\n\n        if (!themes.length) {\n            await this.applyConfigurator('theme_default');\n        } else {\n            this.state.updateRecommendedThemes(themes);\n            this.props.navigate(ROUTES.themeSelectionScreen);\n        }\n    }\n}\n\nexport class ThemeSelectionScreen extends ApplyConfiguratorScreen {\n    static template = \"website.Configurator.ThemeSelectionScreen\";\n    setup() {\n        super.setup();\n\n        this.uiService = useService('ui');\n        this.orm = useService('orm');\n        this.maxNbrDisplayExtraThemes = 100;\n        const env = useEnv();\n        env.store[\"extraThemesLoaded\"] = false;\n        env.store[\"extraThemes\"] = [];\n        this.state = useState(env.store);\n        this.themeSVGPreviews = [useRef('ThemePreview1'), useRef('ThemePreview2'), useRef('ThemePreview3')];\n        this.extraThemesButtonRef = useRef(\"extraThemesButton\");\n        this.extraThemeSVGPreviews = [];\n        for (let i = 0; i < this.maxNbrDisplayExtraThemes; i++) {\n            this.extraThemeSVGPreviews.push(useRef(`ExtraThemePreview${i}`));\n        }\n\n        onMounted(() => {\n            this.blockUiDuringImageLoading(this.state.themes, this.themeSVGPreviews);\n        });\n\n        useEffect(\n            () => this.blockUiDuringImageLoading(this.state.extraThemes, this.extraThemeSVGPreviews),\n            () => [this.state.extraThemes]\n        );\n    }\n\n    /**\n     * The button should be shown if we never tried to load the extra themes and\n     * if they are enough main themes already displayed. If this last condition\n     * is not fulfilled, there is no need to display the button as no more will\n     * be displayed.\n     */\n    get showViewMoreThemesButton() {\n        return !this.state.extraThemesLoaded\n            && this.state.themes.length === MAX_NBR_DISPLAY_MAIN_THEMES;\n    }\n\n    /**\n     * Transforms text svgs into svg elements and adds a loading effect that\n     * blocks the UI during the loading of the images inside those svg elements.\n     *\n     * @param {Array<Object>} themes - The text svgs.\n     * @param {Array} themeSVGPreviews - A reference to the svg elements.\n     */\n    blockUiDuringImageLoading(themes, themeSVGPreviews) {\n        if (!themes.length) {\n            // There is no svg to transform\n            return;\n        }\n        const proms = [];\n        this.uiService.block({delay: 700});\n        themes.forEach((theme, idx) => {\n            const svgEl = new DOMParser().parseFromString(theme.svg, \"image/svg+xml\").documentElement;\n            for (const imgEl of svgEl.querySelectorAll(\"image\")) {\n                proms.push(new Promise((resolve, reject) => {\n                    imgEl.addEventListener(\"load\", () => {\n                        resolve(imgEl);\n                    }, {once: true});\n                    imgEl.addEventListener(\"error\", () => {\n                        reject(imgEl);\n                    }, {once: true});\n                }));\n            }\n            themeSVGPreviews[idx].el.appendChild(svgEl);\n        });\n        // When all the images inside the svgs are loaded then remove the\n        // loading effect.\n        Promise.allSettled(proms).then(() => {\n            this.uiService.unblock();\n        });\n    }\n\n    async chooseTheme(themeName) {\n        await this.applyConfigurator(themeName);\n    }\n\n    async getMoreThemes() {\n        const removeLoadingEffect = addButtonLoadingEffect(this.extraThemesButtonRef.el);\n        const themes = await getRecommendedThemes(\n            this.orm,\n            this.state,\n            this.maxNbrDisplayExtraThemes\n        );\n        // Filter the extra themes to not propose a theme that is already\n        // present in the main themes.\n        const mainThemeNames = this.state.themes.map((theme) => theme.name);\n        this.state.extraThemes = themes.filter((extraTheme) => !mainThemeNames.includes(extraTheme.name));\n        this.state.extraThemesLoaded = true;\n        removeLoadingEffect();\n    }\n\n    getExtraThemeName(idx) {\n        return this.state.extraThemes.length > idx && this.state.extraThemes[idx].name;\n    }\n}\n\n//------------------------------------------------------------------------------\n// Store\n//------------------------------------------------------------------------------\n\nexport class Store {\n    async start(getInitialState) {\n        Object.assign(this, await getInitialState());\n    }\n\n    //-------------------------------------------------------------------------\n    // Getters\n    //-------------------------------------------------------------------------\n\n    getWebsiteTypes() {\n        return Object.values(WEBSITE_TYPES);\n    }\n\n    getSelectedType(id) {\n        return id && WEBSITE_TYPES[id];\n    }\n\n    getWebsitePurpose() {\n        return Object.values(WEBSITE_PURPOSES);\n    }\n\n    getSelectedPurpose(id) {\n        return id && WEBSITE_PURPOSES[id];\n    }\n\n    getFeatures() {\n        return Object.values(this.features);\n    }\n\n    getPalettes() {\n        return Object.values(this.palettes);\n    }\n\n    getThemeName(idx) {\n        return this.themes.length > idx && this.themes[idx].name;\n    }\n\n    /**\n     * @returns {string | false}\n     */\n    getSelectedPaletteName() {\n        const palette = this.selectedPalette;\n        return palette ? (palette.name || 'recommendedPalette') : false;\n    }\n\n    //-------------------------------------------------------------------------\n    // Actions\n    //-------------------------------------------------------------------------\n\n    selectWebsiteType(id) {\n        Object.values(this.features).filter((feature) => feature.module_state !== 'installed').forEach((feature) => {\n            feature.selected = feature.website_config_preselection.includes(WEBSITE_TYPES[id].name);\n        });\n        this.selectedType = id;\n    }\n\n    selectWebsitePurpose(id) {\n        // Keep track or the former selection in order to be able to keep\n        // the auto-advance navigation scheme while being able to use the\n        // browser's back and forward buttons.\n        if (!id && this.selectedPurpose) {\n            this.formerSelectedPurpose = this.selectedPurpose;\n        }\n        Object.values(this.features).filter((feature) => feature.module_state !== 'installed').forEach((feature) => {\n            // need to check id, since we set to undefined in mount() to avoid the auto next screen on back button\n            feature.selected |= id && feature.website_config_preselection.includes(WEBSITE_PURPOSES[id].name);\n        });\n        this.selectedPurpose = id;\n    }\n\n    selectIndustry(label, id) {\n        if (!label || !id) {\n            this.selectedIndustry = undefined;\n        } else {\n            this.selectedIndustry = { id, label };\n        }\n    }\n\n    changeLogo(data, attachmentId) {\n        this.logo = data;\n        this.logoAttachmentId = attachmentId;\n    }\n\n    selectPalette(paletteName) {\n        if (paletteName === 'recommendedPalette') {\n            this.selectedPalette = this.recommendedPalette;\n        } else {\n            this.selectedPalette = this.palettes[paletteName];\n        }\n    }\n\n    toggleFeature(featureId) {\n        const feature = this.features[featureId];\n        const isModuleInstalled = feature.module_state === 'installed';\n        feature.selected = !feature.selected || isModuleInstalled;\n    }\n\n    setRecommendedPalette(color1, color2) {\n        if (color1 && color2) {\n            if (color1 === color2) {\n                color2 = mixCssColors('#FFFFFF', color1, 0.2);\n            }\n            const recommendedPalette = {\n                color1: color1,\n                color2: color2,\n                color3: mixCssColors('#FFFFFF', color2, 0.9),\n                color4: '#FFFFFF',\n                color5: mixCssColors(color1, '#000000', 0.125),\n            };\n            CUSTOM_BG_COLOR_ATTRS.forEach((attr) => {\n                recommendedPalette[attr] = recommendedPalette[this.defaultColors[attr]];\n            });\n            this.recommendedPalette = recommendedPalette;\n        } else {\n            this.recommendedPalette = undefined;\n        }\n        this.selectedPalette = this.recommendedPalette;\n    }\n\n    updateRecommendedThemes(themes) {\n        this.themes = themes.slice(0, MAX_NBR_DISPLAY_MAIN_THEMES);\n    }\n}\n\nfunction useStore() {\n    const env = useEnv();\n    return useState(env.store);\n}\n\nexport class Configurator extends Component {\n    static components = {\n        WelcomeScreen,\n        DescriptionScreen,\n        PaletteSelectionScreen,\n        FeaturesSelectionScreen,\n        ThemeSelectionScreen,\n    };\n    static template = 'website.Configurator.Configurator';\n    static props = { ...standardActionServiceProps };\n\n    setup() {\n        this.orm = useService('orm');\n        this.action = useService('action');\n\n        // Using the back button must update the router state.\n        useExternalListener(window, \"popstate\", (ev) => {\n            // FIXME: this doesn't work unless this component is already mounted so navigating through\n            // history from a different client action will not work.\n            if (ev.state && \"configuratorStep\" in ev.state) {\n                // Do not use navigate because URL is already updated.\n                this.state.currentStep = ev.state.configuratorStep;\n            }\n        });\n\n        const initialStep = this.props.action.context.params && this.props.action.context.params.step;\n        const store = reactive(new Store(), () => this.updateStorage(store));\n\n        this.state = useState({\n            currentStep: initialStep,\n        });\n\n        useSubEnv({ store });\n\n        onWillStart(async () => {\n            this.websiteId = (await this.orm.call('website', 'get_current_website')).match(/\\d+/)[0];\n\n            await store.start(() => this.getInitialState());\n            this.updateStorage(store);\n            if (!store.industries) {\n                await this.skipConfigurator();\n            }\n        });\n\n        // This is a hack to overwrite the history state, modified by the\n        // router service after executing an action. Ideally, the router\n        // service would let us push a state with a new pathname.\n        onMounted(() => {\n            setTimeout(() => {\n                router.cancelPushes();\n                this.updateBrowserUrl();\n            });\n        });\n    }\n\n    get pathname() {\n        return `/website/configurator${this.state.currentStep ? `/${encodeURIComponent(this.state.currentStep)}` : ''}`;\n    }\n\n    get storageItemName() {\n        return `websiteConfigurator${this.websiteId}`;\n    }\n\n    updateBrowserUrl() {\n        history.pushState({ skipRouteChange: true, configuratorStep: this.state.currentStep }, '', this.pathname);\n    }\n\n    navigate(step, reload = false) {\n        this.state.currentStep = step;\n        if (reload) {\n            redirect(this.pathname);\n        } else {\n            this.updateBrowserUrl();\n        }\n    }\n\n    clearStorage() {\n        sessionStorage.removeItem(this.storageItemName);\n    }\n\n    async getInitialState() {\n        // Load values from python and iap\n        var results = await this.orm.call('website', 'configurator_init');\n        const r = {\n            industries: results.industries,\n            logo: results.logo ? 'data:image/png;base64,' + results.logo : false,\n        };\n        r.industries = r.industries.map((industry, index) => ({\n            ...industry,\n            wordCount: industry.label.split(\" \").length,\n            hitCountOrder: index,\n        }));\n\n        // Load palettes from the current CSS\n        const palettes = {};\n        const style = window.getComputedStyle(document.documentElement);\n\n        PALETTE_NAMES.forEach((paletteName) => {\n            const palette = {\n                name: paletteName\n            };\n            for (let j = 1; j <= 5; j += 1) {\n                palette[`color${j}`] = weUtils.getCSSVariableValue(`o-palette-${paletteName}-o-color-${j}`, style);\n            }\n            CUSTOM_BG_COLOR_ATTRS.forEach((attr) => {\n                palette[attr] = weUtils.getCSSVariableValue(`o-palette-${paletteName}-${attr}-bg`, style);\n            });\n            palettes[paletteName] = palette;\n        });\n\n        const localState = JSON.parse(sessionStorage.getItem(this.storageItemName));\n        if (localState) {\n            let themes = [];\n            if (localState.selectedIndustry && localState.selectedPalette) {\n                themes = await getRecommendedThemes(this.orm, localState);\n            }\n            return Object.assign(r, {...localState, palettes, themes});\n        }\n\n        const features = {};\n        results.features.forEach(feature => {\n            features[feature.id] = Object.assign({}, feature, {selected: feature.module_state === 'installed'});\n            const wtp = features[feature.id]['website_config_preselection'];\n            features[feature.id]['website_config_preselection'] = wtp ? wtp.split(',') : [];\n        });\n\n        // Palette color used by default as background color for menu and footer.\n        // Needed to build the recommended palette.\n        const defaultColors = {};\n        CUSTOM_BG_COLOR_ATTRS.forEach((attr) => {\n            const color = weUtils.getCSSVariableValue(`o-default-${attr}-bg`, style);\n            const match = color.match(/o-color-(?<idx>[1-5])/);\n            const colorIdx = parseInt(match.groups['idx']);\n            defaultColors[attr] = `color${colorIdx}`;\n        });\n\n        return Object.assign(r, {\n            selectedType: undefined,\n            selectedPurpose: undefined,\n            formerSelectedPurpose: undefined,\n            selectedIndustry: undefined,\n            selectedPalette: undefined,\n            recommendedPalette: undefined,\n            defaultColors: defaultColors,\n            palettes: palettes,\n            features: features,\n            themes: [],\n            logoAttachmentId: undefined,\n        });\n    }\n\n    updateStorage(state) {\n        const newState = JSON.stringify({\n            defaultColors: state.defaultColors,\n            features: state.features,\n            logo: state.logo,\n            logoAttachmentId: state.logoAttachmentId,\n            selectedIndustry: state.selectedIndustry,\n            selectedPalette: state.selectedPalette,\n            selectedPurpose: state.selectedPurpose,\n            formerSelectedPurpose: state.formerSelectedPurpose,\n            selectedType: state.selectedType,\n            recommendedPalette: state.recommendedPalette,\n        });\n        sessionStorage.setItem(this.storageItemName, newState);\n    }\n\n    async skipConfigurator() {\n        await this.orm.call('website', 'configurator_skip');\n        this.clearStorage();\n        this.action.doAction('website.theme_install_kanban_action', {\n            clearBreadcrumbs: true,\n        });\n    }\n}\n\nregistry.category('actions').add('website_configurator', Configurator);\n", "import { registry } from \"@web/core/registry\";\n\nexport async function openCustomMenu(env, action) {\n    const websiteCustomMenus = env.services[\"website_custom_menus\"];\n    const websiteMenu = websiteCustomMenus.get(action.context.xmlid);\n    if (websiteMenu) {\n        websiteCustomMenus.open({ xmlid: action.context.xmlid });\n    }\n}\n\n// TODO we should probably have a more standard system for this\n// \"website_custom_menus\" feature.\nregistry.category(\"actions\").add(\"open_website_custom_menu\", openCustomMenu);\n", "/** @odoo-module **/\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { Layout } from \"@web/search/layout\";\nimport { Component, useEffect, useState } from \"@odoo/owl\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { DocumentationLink } from \"@web/views/widgets/documentation_link/documentation_link\";\n\nclass WebsiteDashboard extends Component {\n    static template = \"website.WebsiteDashboardMain\";\n    static components = { Layout, DocumentationLink };\n    static props = [\"*\"];\n    setup() {\n        super.setup();\n        this.keepLast = new KeepLast();\n\n        this.state = useState({\n            website: false,\n            groups: {},\n            websites: [],\n            dashboards: {},\n        });\n\n        useEffect(\n            () => {\n                this.fetchData();\n            },\n            () => [this.state.website]\n        );\n    }\n\n    get display() {\n        return {\n            controlPanel: {},\n        };\n    }\n\n    async fetchData() {\n        const dashboardData = await this.keepLast.add(\n            rpc(\"/website/fetch_dashboard_data\", {\n                website_id: this.state.website,\n            })\n        );\n        Object.assign(this.state, dashboardData);\n    }\n}\n\nregistry.category(\"actions\").add(\"backend_dashboard\", WebsiteDashboard);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { browser } from '@web/core/browser/browser';\nimport { registry } from '@web/core/registry';\nimport { ResizablePanel } from '@web/core/resizable_panel/resizable_panel';\nimport { useService, useBus } from '@web/core/utils/hooks';\nimport { redirect } from \"@web/core/utils/urls\";\nimport { session } from \"@web/session\";\nimport { ResourceEditor } from '../../components/resource_editor/resource_editor';\nimport { WebsiteEditorComponent } from '../../components/editor/editor';\nimport { WebsiteTranslator } from '../../components/translator/translator';\nimport { unslugHtmlDataObject } from '../../services/website_service';\nimport {OptimizeSEODialog} from '@website/components/dialog/seo';\nimport { WebsiteDialog } from \"@website/components/dialog/dialog\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport wUtils from '@website/js/utils';\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { SIZES, utils as uiUtils } from \"@web/core/ui/ui_service\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\nimport {\n    Component,\n    onWillStart,\n    onMounted,\n    onWillUnmount,\n    useRef,\n    useEffect,\n    useState,\n    useExternalListener,\n} from \"@odoo/owl\";\nimport { getScrollingElement } from \"@web/core/utils/scrolling\";\n\nclass BlockPreview extends Component {\n    static template = \"website.BlockPreview\";\n    static props = {};\n}\n\nexport class WebsitePreview extends Component {\n    static template = \"website.WebsitePreview\";\n    static components = {\n        WebsiteEditorComponent,\n        BlockPreview,\n        WebsiteTranslator,\n        ResourceEditor,\n        ResizablePanel,\n    };\n    static props = { ...standardActionServiceProps };\n    setup() {\n        this.websiteService = useService('website');\n        this.dialogService = useService('dialog');\n        this.title = useService('title');\n        this.action = useService('action');\n        this.orm = useService('orm');\n\n        this.iframeFallbackUrl = '/website/iframefallback';\n\n        this.iframe = useRef('iframe');\n        this.iframefallback = useRef('iframefallback');\n        this.container = useRef('container');\n        this.websiteContext = useState(this.websiteService.context);\n        this.blockedState = useState({\n            isBlocked: false,\n            showLoader: false,\n        });\n        // The params used to configure the context should be ignored when the\n        // action is restored (example: click on the breadcrumb).\n        this.isRestored = this.props.action.jsId === this.websiteService.actionJsId;\n        this.websiteService.actionJsId = this.props.action.jsId;\n\n        useBus(this.websiteService.bus, 'BLOCK', (event) => this.block(event.detail));\n        useBus(this.websiteService.bus, 'UNBLOCK', () => this.unblock());\n        useExternalListener(window, \"keydown\", this._onKeydownRefresh.bind(this));\n\n        onWillStart(async () => {\n            const [backendWebsiteRepr] = await Promise.all([\n                this.orm.call('website', 'get_current_website'),\n                this.websiteService.fetchWebsites(),\n                this.websiteService.fetchUserGroups(),\n            ]);\n            this.backendWebsiteId = unslugHtmlDataObject(backendWebsiteRepr).id;\n\n            const encodedPath = encodeURIComponent(this.path);\n            if (!session.website_bypass_domain_redirect // Used by the Odoo support (bugs to be expected)\n                    // As a stable fix, we chose to never redirect to the right\n                    // domain anymore in this case. We still do when using the\n                    // website switcher, but not when reaching the \"default\"\n                    // website. The goal is to better support users typing\n                    // mysupercompany.odoo.com explicitly to enter their\n                    // backend instead of mysupercompany.be.\n                    // Bugs are to be expected while editing/using the website\n                    // mysupercompany.be from mysupercompany.odoo.com though,\n                    // but it should be the case only in specific/advanced\n                    // situations.\n                    // TODO remove this code properly in master.\n                    && 1 === 0\n                    && this.websiteDomain\n                    && !wUtils.isHTTPSorNakedDomainRedirection(this.websiteDomain, window.location.origin)) {\n                // The website domain might be the naked one while the naked one\n                // is actually redirecting to `www` (or the other way around).\n                // In such a case, we need to consider those 2 from the same\n                // domain and let the iframe load that \"different\" domain. The\n                // iframe will actually redirect to the correct one (naked/www),\n                // which will ends up with the same domain as the parent window\n                // URL (event if it wasn't, it wouldn't be an issue as those are\n                // really considered as the same domain, the user will share the\n                // same session and CORS errors won't be a thing in such a case)\n                this.dialogService.add(WebsiteDialog, {\n                    title: _t(\"Redirecting...\"),\n                    body: _t(\"You are about to be redirected to the domain configured for your website ( %s ). This is necessary to edit or view your website from the Website app. You might need to log back in.\", this.websiteDomain),\n                    showSecondaryButton: false,\n                }, {\n                    onClose: () => {\n                        window.location.href = `${encodeURI(this.websiteDomain)}/odoo/action-website.website_preview?path=${encodedPath}&website_id=${encodeURIComponent(this.websiteId)}`;\n                    }\n                });\n            } else {\n                this.initialUrl = `/website/force/${encodeURIComponent(this.websiteId)}?path=${encodedPath}`;\n            }\n        });\n\n        useEffect(() => {\n            this.websiteService.currentWebsiteId = this.websiteId;\n            if (this.isRestored) {\n                return;\n            }\n\n            const isScreenLargeEnoughForEdit =\n                uiUtils.getSize() >= SIZES.MD;\n            if (!isScreenLargeEnoughForEdit && this.props.action.context.params) {\n                this.props.action.context.params.enable_editor = false;\n                this.props.action.context.params.with_loader = false;\n            }\n\n            this.websiteService.context.showNewContentModal = this.props.action.context.params && this.props.action.context.params.display_new_content;\n            this.websiteService.context.edition = this.props.action.context.params && !!this.props.action.context.params.enable_editor;\n            this.websiteService.context.translation = this.props.action.context.params && !!this.props.action.context.params.edit_translations;\n            if (this.props.action.context.params && this.props.action.context.params.enable_seo) {\n                this.iframe.el.addEventListener('load', () => {\n                    this.websiteService.pageDocument = this.iframe.el.contentDocument;\n                    this.dialogService.add(OptimizeSEODialog);\n                }, {once: true});\n            }\n            if (this.props.action.context.params && this.props.action.context.params.with_loader) {\n                this.websiteService.showLoader({ showTips: true });\n            }\n        }, () => [this.props.action.context.params]);\n\n        useEffect(() => {\n            this.websiteContext.showResourceEditor = false;\n        }, () => [\n            this.websiteContext.showNewContentModal,\n            this.websiteContext.edition,\n            this.websiteContext.translation,\n        ]);\n\n        onMounted(() => {\n            this.websiteService.blockPreview(true, 'load-iframe');\n            this.iframe.el.addEventListener('load', () => this.websiteService.unblockPreview('load-iframe'), { once: true });\n            // For a frontend page, it is better to use the\n            // OdooFrameContentLoaded event to unblock the iframe, as it is\n            // triggered faster than the load event.\n            this.iframe.el.addEventListener('OdooFrameContentLoaded', () => this.websiteService.unblockPreview('load-iframe'), { once: true });\n        });\n\n        onWillUnmount(() => {\n            this.websiteService.context.showResourceEditor = false;\n            const { pathname, search, hash } = this.iframe.el.contentWindow.location;\n            this.websiteService.lastUrl = `${pathname}${search}${hash}`;\n            this.websiteService.currentWebsiteId = null;\n            this.websiteService.websiteRootInstance = undefined;\n            this.websiteService.pageDocument = null;\n        });\n\n        /**\n         * This removes the 'Odoo' prefix of the title service to display\n         * cleanly the frontend's document title (see _replaceBrowserUrl), and\n         * replaces the backend favicon with the frontend's one.\n         * These changes are reverted when the component is unmounted.\n         */\n        useEffect(() => {\n            const backendIconEl = document.querySelector(\"link[rel~='icon']\");\n            // Save initial backend values.\n            const backendIconHref = backendIconEl.href;\n            this.iframe.el.addEventListener('load', () => {\n                // Replace backend values with frontend's ones.\n                const frontendIconEl = this.iframe.el.contentDocument.querySelector(\"link[rel~='icon']\");\n                if (frontendIconEl) {\n                    backendIconEl.href = frontendIconEl.href;\n                }\n            }, { once: true });\n            return () => {\n                // Restore backend initial values when leaving.\n                backendIconEl.href = backendIconHref;\n            };\n        }, () => []);\n\n        const toggleIsMobile = () => {\n            this.iframe.el.contentDocument.documentElement\n                .classList.toggle('o_is_mobile', this.websiteContext.isMobile);\n        };\n        // Toggle the 'o_is_mobile' class when the context 'isMobile' changes\n        // (e.g. Click on mobile preview buttons).\n        useEffect(toggleIsMobile, () => [this.websiteContext.isMobile]);\n\n        // Toggle the 'o_is_mobile' class according to 'isMobile' on iframe load\n        useEffect(() => {\n            this.iframe.el.addEventListener('OdooFrameContentLoaded', toggleIsMobile);\n            return () => this.iframe.el.removeEventListener('OdooFrameContentLoaded', toggleIsMobile);\n        }, () => []);\n    }\n\n    get websiteId() {\n        let websiteId = this.props.action.context.params && this.props.action.context.params.website_id;\n        // When no parameter is passed to the client action, the current\n        // website from the backend (which is the last viewed/edited) will be\n        // taken.\n        if (!websiteId) {\n            websiteId = this.backendWebsiteId;\n        }\n        if (!websiteId) {\n            websiteId = this.websiteService.websites[0].id;\n        }\n        return websiteId;\n    }\n\n    get websiteDomain() {\n        return this.websiteService.websites.find(website => website.id === this.websiteId).domain;\n    }\n\n    get path() {\n        let path = this.isRestored\n            ? this.websiteService.lastUrl\n            : this.props.action.context.params && this.props.action.context.params.path;\n\n        if (path) {\n            const url = new URL(path, window.location.origin);\n            if (this._isTopWindowURL(url)) {\n                // If the client action is initialized with a path that should\n                // not be opened inside the iframe (= something we would want to\n                // open on the top window), we consider that this is not a valid\n                // flow. Instead of trying to open it on the top window, we\n                // initialize the iframe with the website homepage...\n                path = '/';\n            } else {\n                // ... otherwise, the path still needs to be normalized (as it\n                // would be if the given path was used as an href of a  <a/>\n                // element).\n                path = url.pathname + url.search;\n            }\n        } else {\n            path = '/';\n        }\n        return path;\n    }\n\n    get testMode() {\n        return false;\n    }\n\n    get aceEditorWidth() {\n        const storedWidth = browser.localStorage.getItem(\"ace_editor_width\");\n        return storedWidth ? parseInt(storedWidth) : 720;\n    }\n\n    reloadIframe(url) {\n        return new Promise((resolve, reject) => {\n            this.websiteService.websiteRootInstance = undefined;\n            this.iframe.el.addEventListener('OdooFrameContentLoaded', resolve, { once: true });\n            if (url) {\n                this.iframe.el.contentWindow.location = url;\n            } else {\n                this.iframe.el.contentWindow.location.reload();\n            }\n        });\n    }\n\n    block({ showLoader = true } = {}) {\n        this.blockedState.isBlocked = true;\n        this.blockedState.showLoader = showLoader;\n    }\n\n    unblock() {\n        this.blockedState.isBlocked = false;\n        this.blockedState.showLoader = false;\n    }\n\n    addWelcomeMessage() {\n        if (this.websiteService.isRestrictedEditor) {\n            const wrap = this.iframe.el.contentDocument.querySelector('#wrapwrap.homepage #wrap');\n            if (wrap && !wrap.innerHTML.trim()) {\n                this.welcomeMessage = renderToElement('website.homepage_editor_welcome_message');\n                this.welcomeMessage.classList.add('o_homepage_editor_welcome_message', 'h-100');\n                while (wrap.firstChild) {\n                    wrap.removeChild(wrap.lastChild);\n                }\n                wrap.append(this.welcomeMessage);\n            }\n        }\n    }\n\n    removeWelcomeMessage() {\n        if (this.welcomeMessage) {\n            this.welcomeMessage.remove();\n        }\n    }\n\n    /**\n     * Returns true if the url should be opened in the top\n     * window.\n     *\n     * @param host {string} host of the route.\n     * @param pathname {string} path of the route.\n     * @private\n     */\n    _isTopWindowURL({ host, pathname }) {\n        const backendRoutes = ['/web', '/web/session/logout', '/odoo'];\n        return host !== window.location.host\n            || (pathname\n                && (backendRoutes.includes(pathname)\n                    || pathname.startsWith('/@/')\n                    || pathname.startsWith('/odoo/')\n                    || pathname.startsWith('/web/content/')\n                    // This is defined here to avoid creating a\n                    // website_documents module for just one patch.\n                    || pathname.startsWith('/document/share/')));\n    }\n\n    /**\n     * This replaces the browser url (/odoo/action-website...) with\n     * the iframe's url (it is clearer for the user).\n     */\n    _replaceBrowserUrl() {\n        if (!wUtils.isHTTPSorNakedDomainRedirection(this.iframe.el.contentWindow.location.origin, window.location.origin)) {\n            // If another domain ends up loading in the iframe (for example,\n            // if the iframe is being redirected and has no initial URL, so it\n            // loads \"about:blank\"), do not push that into the history\n            // state as that could prevent the user from going back and could\n            // trigger a traceback.\n            history.replaceState(history.state, document.title, '/odoo');\n            return;\n        }\n        const currentTitle = this.iframe.el.contentDocument.title;\n        history.replaceState(history.state, currentTitle, this.iframe.el.contentDocument.location.href);\n        this.title.setParts({ action: currentTitle });\n    }\n\n    _onPageLoaded(ev) {\n        // FIX Chrome-only. If you have the backend in a language A but the\n        // website in English only, you can 1) modify a record's (event,\n        // product...) name in language A (say \"New Name\").\n        // 2) visit the page `/new-name-11` => the server will redirect you to\n        // the English page `/origin-11`, which is the only one existing.\n        // Chrome caches the redirection.\n        // 3) give the same name in English as in language A, try to visit\n        // => the server now wants to access `/new-name-11`\n        // => Chrome uses the cache to redirect `/new-name-11` to `/origin-11`,\n        // => the server tries to redirect to `/new-name-11` => loop.\n        // Chrome injects a \"Too many redirects\" layout in the iframe, which in\n        // turn raises a CORS error when the app tries to update the iframe.\n        // If we detect that behavior, we reload the iframe with a new query\n        // parameter, so that it's not cached for Chrome.\n        if (\n            navigator.userAgent.toLowerCase().includes(\"chrome\")\n            && !this.iframe.el.src.includes(\"iframe_reload\")\n        ) {\n            try {\n                /* eslint-disable no-unused-expressions */\n                this.iframe.el.contentWindow.location.href;\n            } catch (err) {\n                if (err.name === \"SecurityError\") {\n                    ev.stopImmediatePropagation();\n                    // Note that iframe's `src` is the URL used to start the\n                    // website preview, it's not sync'd with iframe navigation.\n                    const srcUrl = new URL(this.iframe.el.src);\n                    const pathUrl = new URL(srcUrl.searchParams.get(\"path\"), srcUrl.origin);\n                    pathUrl.searchParams.set(\"iframe_reload\", \"1\");\n                    srcUrl.searchParams.set(\"path\", `${pathUrl.pathname}${pathUrl.search}`);\n                    // We could inject `pathUrl` directly but keep the same\n                    // expected URL format `/website/force/1?path=..`\n                    this.iframe.el.src = srcUrl.toString();\n                    return;\n                }\n            }\n        }\n        if (this.lastHiddenPageURL !== this.iframe.el.contentWindow.location.href) {\n            // Hide Ace Editor when moving to another page.\n            this.websiteService.context.showResourceEditor = false;\n            this.lastHiddenPageURL = undefined;\n        }\n        if (this.props.action.context.params?.with_loader) {\n            this.websiteService.hideLoader();\n            this.props.action.context.params.with_loader = false;\n        }\n        this.iframe.el.contentWindow.addEventListener('beforeunload', this._onPageUnload.bind(this));\n        this._replaceBrowserUrl();\n        this.iframe.el.contentWindow.addEventListener('popstate', this._replaceBrowserUrl.bind(this));\n        this.iframe.el.contentWindow.addEventListener('pagehide', this._onPageHide.bind(this));\n\n        this.websiteService.pageDocument = this.iframe.el.contentDocument;\n\n        // This is needed for the registerThemeHomepageTour tours\n        const { editable, viewXmlid } = this.websiteService.currentWebsite.metadata;\n        this.container.el.dataset.viewXmlid = viewXmlid;\n        // The iframefallback is hidden in test mode\n        if (!editable && this.iframefallback.el) {\n            this.iframefallback.el.classList.add('d-none');\n        }\n\n        this.iframe.el.contentWindow.addEventListener('PUBLIC-ROOT-READY', (event) => {\n            this.iframe.el.setAttribute('is-ready', 'true');\n            if (!this.websiteContext.edition && editable) {\n                this.addWelcomeMessage();\n            }\n            this.websiteService.websiteRootInstance = event.detail.rootInstance;\n        });\n\n        // The clicks on the iframe are listened, so that links with external\n        // redirections can be opened in the top window.\n        this.iframe.el.contentDocument.addEventListener('click', (ev) => {\n            const isEditing = this.websiteContext.edition || this.websiteContext.translation;\n            if (!isEditing) {\n                // Forward clicks to close backend client action's navbar\n                // dropdowns.\n                this.iframe.el.dispatchEvent(new MouseEvent('click', ev));\n            } else {\n                // When in edit mode, prevent the default behaviours of clicks\n                // as to avoid DOM changes not handled by the editor.\n                // (Such as clicking on a link that triggers navigating to\n                // another page.)\n                if (!ev.target.closest('#oe_manipulators')) {\n                    ev.preventDefault();\n                }\n            }\n\n            const linkEl = ev.target.closest('[href]');\n            if (!linkEl) {\n                return;\n            }\n\n            const { href, target, classList } = linkEl;\n            if (classList.contains('o_add_language')) {\n                ev.preventDefault();\n                const searchParams = new URLSearchParams(href);\n                this.action.doAction('base.action_view_base_language_install', {\n                    target: 'new',\n                    additionalContext: {\n                        params: {\n                            website_id: this.websiteId,\n                            url_return: searchParams.get(\"url_return\"),\n                        },\n                    },\n                });\n            } else if (classList.contains('js_change_lang') && isEditing) {\n                ev.preventDefault();\n                const lang = linkEl.dataset['url_code'];\n                // The \"edit_translations\" search param coming from keep_query\n                // is removed, and the hash is added.\n                const destinationUrl = new URL(href, window.location);\n                destinationUrl.searchParams.delete('edit_translations');\n                destinationUrl.hash = this.websiteService.contentWindow.location.hash;\n                this.websiteService.bus.trigger('LEAVE-EDIT-MODE', {\n                    onLeave: () => {\n                        this.websiteService.goToWebsite({ path: destinationUrl.toString(), lang });\n                    },\n                    reloadIframe: false,\n                });\n            } else if (href && target !== '_blank' && !isEditing) {\n                if (this._isTopWindowURL(linkEl)) {\n                    ev.preventDefault();\n                    browser.location.assign(href);\n                } else if (this.iframe.el.contentWindow.location.pathname !== new URL(href).pathname) {\n                    // This scenario triggers a navigation inside the iframe.\n                    this.websiteService.websiteRootInstance = undefined;\n                }\n            }\n        });\n        this.iframe.el.contentDocument.addEventListener('keydown', ev => {\n            if (getActiveHotkey(ev) === 'control+k' && !this.websiteContext.edition) {\n                // Avoid for browsers to focus on the URL bar when pressing\n                // CTRL-K from within the iframe.\n                ev.preventDefault();\n            }\n            // Check if it's a refresh first as we want to prevent default in that case.\n            this._onKeydownRefresh(ev);\n            this.iframe.el.dispatchEvent(new KeyboardEvent('keydown', ev));\n        });\n        this.iframe.el.contentDocument.addEventListener('keyup', ev => {\n            this.iframe.el.dispatchEvent(new KeyboardEvent('keyup', ev));\n        });\n    }\n\n    /**\n     * This method is called when the page is unloaded to clean\n     * the iframefallback content.\n     */\n    _cleanIframeFallback() {\n        // Remove autoplay in all iframes urls so videos are not\n        // playing in the background\n        const iframesEl = this.iframefallback.el.contentDocument.querySelectorAll('iframe[src]:not([src=\"\"])');\n        for (const iframeEl of iframesEl) {\n            const url = new URL(iframeEl.src);\n            url.searchParams.delete('autoplay');\n            iframeEl.src = url.toString();\n        }\n    }\n\n    _onResourceEditorResize(width) {\n        browser.localStorage.setItem(\"ace_editor_width\", width);\n    }\n\n    _onPageUnload() {\n        this.iframe.el.setAttribute('is-ready', 'false');\n        // Before leaving the iframe, its content is replicated on an\n        // underlying iframe, to avoid for white flashes (visible on\n        // Chrome Windows/Linux).\n        // If the iframe is currently displaying an XML file, the body does not\n        // exist, so we do not replace the iframefallback content.\n        // The iframefallback is hidden in test mode\n        if (!this.websiteContext.edition && this.iframe.el.contentDocument.body && this.iframefallback.el) {\n            this.iframefallback.el.contentDocument.body.replaceWith(this.iframe.el.contentDocument.body.cloneNode(true));\n            this.iframefallback.el.classList.remove('d-none');\n            getScrollingElement(this.iframefallback.el.contentDocument).scrollTop = getScrollingElement(this.iframe.el.contentDocument).scrollTop;\n            this._cleanIframeFallback();\n        }\n    }\n    _onPageHide() {\n        this.lastHiddenPageURL = this.iframe.el && this.iframe.el.contentWindow.location.href;\n        // Normally, at this point, the websiteRootInstance is already set to\n        // `undefined`, as we want to do that as early as possible to prevent\n        // the editor to be in an unstable state. But some events not managed\n        // by the websitePreview could trigger a `pagehide`, so for safety,\n        // it is set to undefined again.\n        this.websiteService.websiteRootInstance = undefined;\n    }\n    /**\n     * Handles refreshing while the website preview is active.\n     * Makes it possible to stay in the backend after an F5 or CTRL-R keypress.\n     *\n     * @param  {KeyboardEvent} ev\n     * @private\n     */\n    _onKeydownRefresh(ev) {\n        const hotkey = getActiveHotkey(ev);\n        if (hotkey !== 'control+r' && hotkey !== 'f5') {\n            return;\n        }\n        // The iframe isn't loaded yet: fallback to default refresh.\n        if (this.websiteService.contentWindow === undefined) {\n            return;\n        }\n        ev.preventDefault();\n        const path = this.websiteService.contentWindow.location;\n        const debugMode = this.env.debug ? `&debug=${this.env.debug}` : \"\";\n        redirect(`/odoo/action-website.website_preview?path=${encodeURIComponent(path)}${debugMode}`);\n    }\n}\n\nregistry.category('actions').add('website_preview', WebsitePreview);\n", "/** @odoo-module **/\n\nimport {PageDependencies} from '@website/components/dialog/page_properties';\nimport {standardFieldProps} from '@web/views/fields/standard_field_props';\nimport { UrlField, urlField } from \"@web/views/fields/url/url_field\";\nimport {registry} from '@web/core/registry';\nimport { _t } from '@web/core/l10n/translation';\nimport { Component, useEffect, useRef } from \"@odoo/owl\";\n\n/**\n * Displays website page dependencies and URL redirect options when the page URL\n * is updated.\n */\nclass PageUrlField extends UrlField {\n    static components = { PageDependencies };\n    static template = \"website.PageUrlField\";\n    static defaultProps = {\n        ...UrlField.defaultProps,\n        websitePath: true,\n    };\n\n    setup() {\n        super.setup();\n        this.serverUrl = `${window.location.origin}/`;\n        this.inputRef = useRef(\"input\");\n\n        // Trigger onchange api on input event to display redirection\n        // parameters as soon as the user types.\n        // TODO should find a way to do this more automatically (and option in\n        // the framework? or at least a t-on-input?)\n        useEffect(\n            (inputEl) => {\n                if (inputEl) {\n                    const fireChangeEvent = () => {\n                        inputEl.dispatchEvent(new Event(\"change\"));\n                    };\n\n                    inputEl.addEventListener(\"input\", fireChangeEvent);\n                    return () => {\n                        inputEl.removeEventListener(\"input\", fireChangeEvent);\n                    };\n                }\n            },\n            () => [this.inputRef.el],\n        );\n    }\n\n    get value() {\n        let value = super.value;\n        // Strip leading slash\n        if (value[0] === \"/\") {\n            value = value.substring(1);\n        }\n        // Re-add the leading slash for saving, because url field is required\n        // and thus doesn't accept an empty string.\n        this.props.record.data[this.props.name] = `/${value.trim()}`;\n        return value;\n    }\n}\n\nconst pageUrlField = {\n    ...urlField,\n    component: PageUrlField,\n};\n\nregistry.category(\"fields\").add(\"page_url\", pageUrlField);\n\n/**\n * Displays 'Selection' field's values as images to select.\n * Image src for each value can be added using the option 'images' on field XML.\n */\nexport class ImageRadioField extends Component {\n    static template = \"website.FieldImageRadio\";\n    static props = {\n        ...standardFieldProps,\n        images: { type: Array, element: String },\n    };\n\n    setup() {\n        const selection = this.props.record.fields[this.props.name].selection;\n        // Check if value / label exists for each selection item and add the\n        // corresponding image from field options.\n        this.values = selection.filter(item => {\n            return item[0] || item[1];\n        }).map((value, index) => {\n            return [...value, this.props.images && this.props.images[index] || ''];\n        });\n    }\n\n    /**\n     * @param {String} value\n     */\n    onSelectValue(value) {\n        this.props.record.update({ [this.props.name]: value });\n    }\n}\n\nexport const imageRadioField = {\n    component: ImageRadioField,\n    supportedOptions: [\n        {\n            label: _t(\"Images\"),\n            name: \"images\",\n            type: \"string\",\n            help: _t(\"Use an array to list the images to use in the radio selection.\")\n        }\n    ],\n    supportedTypes: ['selection'],\n    extractProps: ({ options }) => ({\n        images: options.images,\n    }),\n};\n\nregistry.category(\"fields\").add(\"image_radio\", imageRadioField);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { Component } from \"@odoo/owl\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nclass PublishField extends Component {\n    static template = \"website.PublishField\";\n    static props = {...standardFieldProps};\n}\n\nregistry.category(\"fields\").add(\"website_publish_button\", {\n    component: PublishField,\n});\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { pick } from \"@web/core/utils/objects\";\nimport { Component } from \"@odoo/owl\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nclass RedirectField extends Component {\n    static template = \"website.RedirectField\";\n    static props = {...standardFieldProps};\n    get info() {\n        return this.props.record.data[this.props.name] ? _t(\"Published\") : _t(\"Unpublished\");\n    }\n\n    onClick() {\n        this.env.onClickViewButton({\n            clickParams: {\n                type: \"object\",\n                name: \"open_website_url\",\n            },\n            getResParams: () =>\n                pick(this.props.record, \"context\", \"evalContext\", \"resModel\", \"resId\", \"resIds\"),\n        });\n    }\n}\n\n\nregistry.category(\"fields\").add(\"website_redirect_button\", {\n    component: RedirectField,\n});\n", "/** @odoo-module **/\n\nimport { registry } from '@web/core/registry';\nimport { useBus } from \"@web/core/utils/hooks\";\nimport { Component, useState } from \"@odoo/owl\";\nimport { standardFieldProps } from '@web/views/fields/standard_field_props';\n\nclass FieldIframePreview extends Component {\n    static template = \"website.iframeWidget\";\n    static props = {...standardFieldProps};\n    setup() {\n        this.state = useState({isMobile: false});\n\n        useBus(this.env.bus, 'THEME_PREVIEW:SWITCH_MODE', (ev) => {\n            this.state.isMobile = ev.detail.mode === 'mobile';\n        });\n    }\n}\n\nexport const fieldIframePreview = {\n    component: FieldIframePreview,\n};\n\nregistry.category(\"fields\").add(\"iframe\", fieldIframePreview);\n", "/** @odoo-module **/\n\nimport { useBus } from \"@web/core/utils/hooks\";\nimport { EventBus, Component, useState, markup } from \"@odoo/owl\";\nimport { escape, sprintf } from \"@web/core/utils/strings\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class FullscreenIndication extends Component {\n    static props = {\n        bus: EventBus,\n    };\n    static template = \"website.FullscreenIndication\";\n\n    setup() {\n        this.state = useState({ isVisible: false });\n        useBus(this.props.bus, \"FULLSCREEN-INDICATION-SHOW\", this.show.bind(this));\n        useBus(this.props.bus, \"FULLSCREEN-INDICATION-HIDE\", this.hide.bind(this));\n    }\n\n    show() {\n        setTimeout(() => this.state.isVisible = true);\n        this.autofade = setTimeout(() => this.state.isVisible = false, 2000);\n    }\n\n    hide() {\n        if (this.state.isVisible) {\n            this.state.isVisible = false;\n            clearTimeout(this.autofade);\n        }\n    }\n\n    get fullScreenIndicationText() {\n        return markup(sprintf(escape(_t(\"Press %(key)s to exit full screen\")), {key: \"<span>esc</span>\"}));\n    }\n}\n", "/** @odoo-module **/\n\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { sprintf } from \"@web/core/utils/strings\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { EventBus, Component, markup, useEffect, useState } from \"@odoo/owl\";\n\nexport class WebsiteLoader extends Component {\n    static props = {\n        bus: EventBus,\n    };\n    static template = \"website.website_loader\";\n\n    setup() {\n        this.website = useService(\"website\");\n\n        const initialState = {\n            isVisible: false,\n            title: '',\n            flag: false,\n            showTips: false,\n            selectedFeatures: [],\n            showWaitingMessages: false,\n            progressPercentage: 0,\n            bottomMessageTemplate: undefined,\n            showLoader: true,\n            showCloseButton: false,\n        };\n\n        const defaultMessages = [{\n            title: _t(\"Building your website.\"),\n            description: _t(\"Applying your colors and design...\"),\n            flag: \"colors\",\n        }, {\n            title: _t(\"Building your website.\"),\n            description: _t(\"Searching your images....\"),\n            flag: \"images\",\n        }, {\n            title: _t(\"Building your website.\"),\n            description: _t(\"Generating inspiring text...\"),\n            flag: \"text\",\n        }];\n\n        let messagesInterval;\n\n        this.state = useState({\n            ...initialState,\n        });\n        this.waitingMessages = useState(defaultMessages);\n        this.currentWaitingMessage = useState({ ...defaultMessages[0] });\n        this.featuresInstallInfo = { nbInstalled: 0, total: undefined };\n\n        useEffect(\n            (selectedFeatures) => {\n                if (this.state.showWaitingMessages) {\n                    let messagesToDisplay = [...defaultMessages]; // Start with defaultMessages\n                    if (selectedFeatures.length > 0) {\n                        // Merge defaultMessages with the relevant waitingMessages\n                        messagesToDisplay.push(...this.getWaitingMessages(selectedFeatures));\n                    }\n\n                    this.waitingMessages.splice(0, this.waitingMessages.length, ...messagesToDisplay);\n\n                    // Request the number of modules/dependencies to install\n                    // and already installed\n                    this.trackModules(selectedFeatures).catch(console.error);\n\n                    return () => {\n                        clearTimeout(this.trackModulesTimeout);\n                        clearInterval(this.updateProgressInterval);\n                    };\n                }\n            },\n            () => [this.state.selectedFeatures]\n        );\n\n        // Cycle through the waitingMessages every 10s\n        useEffect(\n            () => {\n                if (this.state.showWaitingMessages) {\n                    let msgIndex = 0;\n                    messagesInterval = setInterval(() => {\n                        msgIndex++;\n                        const nextMessage = this.waitingMessages[msgIndex];\n                        Object.assign(this.currentWaitingMessage, nextMessage);\n                        if (this.waitingMessages.length - 1 === msgIndex) {\n                            clearInterval(messagesInterval);\n                        }\n                    }, 6000);\n\n                    return () => clearInterval(messagesInterval);\n                }\n            },\n            () => [this.waitingMessages.length]\n        );\n\n        // Prevent user from closing/refreshing the window\n        useEffect(\n            (isVisible) => {\n                if (isVisible) {\n                    window.addEventListener(\"beforeunload\", this.showRefreshConfirmation);\n                    if (!this.state.selectedFeatures || this.state.selectedFeatures.length === 0) {\n                        // If there is no feature selected, we fake the progress\n                        // for theme installation and configurator_apply. If\n                        // there is at least 1 feature selected, the progress\n                        // bar will be initialized in trackModules().\n                        this.initProgressBar();\n                    }\n                } else {\n                    window.removeEventListener(\"beforeunload\", this.showRefreshConfirmation);\n                }\n\n                return () => {\n                    window.removeEventListener(\"beforeunload\", this.showRefreshConfirmation);\n                    clearInterval(this.updateProgressInterval);\n                };\n            },\n            () => [this.state.isVisible]\n        );\n\n        useBus(this.props.bus, \"SHOW-WEBSITE-LOADER\", (ev) => {\n            const props = ev.detail;\n            this.state.isVisible = true;\n            for (const prop of [\n                \"title\",\n                // FIXME: website user/interactive tours are not properly\n                // working at the moment. This disables the \"follow the tips\"\n                // message in the website loader while waiting for a fix.\n                // \"showTips\",\n                \"selectedFeatures\",\n                \"showWaitingMessages\",\n                \"bottomMessageTemplate\",\n                \"showCloseButton\",\n                \"flag\",\n            ]) {\n                this.state[prop] = props && props[prop];\n            }\n            this.state.showLoader = props && props.showLoader !== false;\n        });\n        useBus(this.props.bus, \"HIDE-WEBSITE-LOADER\", () => {\n            for (const key of Object.keys(initialState)) {\n                this.state[key] = initialState[key];\n            }\n            clearInterval(messagesInterval);\n            clearTimeout(this.trackModulesTimeout);\n            clearInterval(this.updateProgressInterval);\n        });\n        // Action needed if the app automatically refreshes or redirects the\n        // page without hiding/removing the WebsiteLoader. This should be\n        // called prior to any refresh/redirect if the loader is still visible.\n        useBus(this.props.bus, \"PREPARE-OUT-WEBSITE-LOADER\", () => {\n            window.removeEventListener(\"beforeunload\", this.showRefreshConfirmation);\n        });\n    }\n\n    /**\n     * Initializes the progress bar.\n     */\n    initProgressBar() {\n        if (this.updateProgressInterval) {\n            return;\n        }\n        // The progress speed decreases as it approaches its limit. This way,\n        // users have the feeling that the website creation progressing is fast\n        // and we prevent them from leaving the page too early (because they\n        // already did XX% of the process).\n        // If there is no module to install, we fake the progress from 0 to 100.\n        // If there is at least 1 module to install, we take 70% of the progress\n        // bar that we divide by the number of modules to install. We fake the\n        // progress of each module individually and when all modules are\n        // installed, we fake the progress of the remaining 30%.\n        const nbModulesToInstall = this.featuresInstallInfo.total || 0;\n        const isSomethingToInstall = nbModulesToInstall > 0;\n        let currentProgress = 0;\n        // This controls the speed of the progress bar.\n        const progressStep = isSomethingToInstall ? 0.04 : 0.02;\n        let progressForAfterModules = isSomethingToInstall ? 30 : 100;\n        let progressForAllModules = 100 - progressForAfterModules;\n        let lastTotalInstalled = 0;\n        let progressPerModule = isSomethingToInstall ?\n            progressForAllModules / nbModulesToInstall : 0;\n\n        this.updateProgressInterval = setInterval(() => {\n            if (this.featuresInstallInfo.nbInstalled !== lastTotalInstalled) {\n                // A module just finished its install.\n                currentProgress = 0;\n                lastTotalInstalled = this.featuresInstallInfo.nbInstalled;\n            }\n            currentProgress += progressStep;\n            const limit = this.featuresInstallInfo.nbInstalled === nbModulesToInstall ?\n                progressForAfterModules : progressPerModule;\n            this.state.progressPercentage = (lastTotalInstalled * progressPerModule) +\n                Math.atan(currentProgress) / (Math.PI / 2) * limit;\n        }, 100);\n    }\n    /**\n     * Makes a RPC call to track the features and dependencies being installed\n     * and, as long as the number of features installed is different from the\n     * total expected, recursively calls itself again after 1s.\n     *\n     * @param {integer[]} selectedFeatures\n     */\n    async trackModules(selectedFeatures) {\n        const installInfo = await rpc(\n            \"/website/track_installing_modules\",\n            {\n                'selected_features': selectedFeatures,\n                'total_features': this.featuresInstallInfo.total,\n            },\n            { silent: true }\n        );\n        if (!this.featuresInstallInfo.total\n            || this.featuresInstallInfo.nbInstalled !== installInfo.nbInstalled) {\n            this.featuresInstallInfo = installInfo;\n        }\n        this.initProgressBar();\n        if (this.featuresInstallInfo.nbInstalled !== this.featuresInstallInfo.total) {\n            this.trackModulesTimeout = setTimeout(() => this.trackModules(selectedFeatures), 1000);\n        }\n    }\n\n    /**\n     * Depending on the features selected, returns the right waiting messages.\n     *\n     * @param {integer[]} selectedFeatures\n     * @returns {Object[]} - the messages filtered by the selected features\n     */\n    getWaitingMessages(selectedFeatures) {\n        const websiteFeaturesMessages = [{\n            id: 5,\n            title: _t(\"Adding features.\"),\n            name: _t(\"blog\"),\n            description: _t(\"Enabling your %s.\"),\n            flag: \"generic\",\n        }, {\n            id: 7,\n            title: _t(\"Adding features.\"),\n            name: _t(\"recruitment platform\"),\n            description: _t(\"Integrating your %s.\"),\n            flag: \"generic\",\n        }, {\n            id: 8,\n            title: _t(\"Adding features.\"),\n            name: _t(\"online store\"),\n            description: _t(\"Activating your %s.\"),\n            flag: \"generic\",\n        }, {\n            id: 9,\n            title: _t(\"Adding features.\"),\n            name: _t(\"online appointment system\"),\n            description: _t(\"Configuring your %s.\"),\n            flag: \"generic\",\n        }, {\n            id: 10,\n            title: _t(\"Adding features.\"),\n            name: _t(\"forum\"),\n            description: _t(\"Setting up your %s.\"),\n            flag: \"generic\",\n        }, {\n            id: 12,\n            title: _t(\"Adding features.\"),\n            name: _t(\"e-learning platform\"),\n            description: _t(\"Installing your %s.\"),\n            flag: \"generic\",\n        }, {\n            // Always the last message if there is at least 1 feature selected.\n            id: \"last\",\n            title: _t(\"Finalizing.\"),\n            description: _t(\"Activating the last features.\"),\n            flag: \"generic\",\n        }];\n\n        const filteredIds = [...selectedFeatures, \"last\"];\n        const messagesList = websiteFeaturesMessages.filter((msg) => {\n            if (filteredIds.includes(msg.id)) {\n                if (msg.name) {\n                    const highlight = sprintf(\n                        '<span class=\"o_website_loader_text_highlight\">%s</span>', msg.name\n                    );\n                    msg.description = markup(sprintf(msg.description, highlight));\n                }\n                return true;\n            }\n        });\n        return messagesList;\n    }\n\n    /**\n     * Prevents refreshing/leaving the page if the loader is displayed (and\n     * thus some work is being done in the backend) by opening a prompt dialog.\n     *\n     * @param {Event} ev\n     * @returns empty returnValue for Chrome & Safari\n     * cf. https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event#compatibility_notes\n     */\n    showRefreshConfirmation = (ev) => {\n        if (this.state.isVisible) {\n            ev.preventDefault(); // Firefox\n            ev.returnValue = \"\";\n            return ev.returnValue;\n        }\n    };\n\n    /**\n     * Hide the loader.\n     */\n    close() {\n        this.website.hideLoader();\n    }\n}\n", "/** @odoo-module **/\n\nimport {PageControllerMixin} from \"./page_views_mixin\";\nimport {PageSearchModel} from \"./page_search_model\";\nimport {registry} from '@web/core/registry';\nimport {kanbanView} from \"@web/views/kanban/kanban_view\";\nimport {CheckboxItem} from \"@web/core/dropdown/checkbox_item\";\n\nexport class PageKanbanController extends PageControllerMixin(kanbanView.Controller) {\n    static template = \"website.PageKanbanView\";\n    static components = {\n        ...kanbanView.Controller.components,\n        CheckboxItem,\n    };\n    /**\n     * @override\n     */\n    async createRecord() {\n        return this.createWebsiteContent();\n    }\n}\n\nexport const PageKanbanView = {\n    ...kanbanView,\n    Controller: PageKanbanController,\n    SearchModel: PageSearchModel,\n};\n\nregistry.category(\"views\").add(\"website_pages_kanban\", PageKanbanView);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport {PageControllerMixin} from \"./page_views_mixin\";\nimport {PageSearchModel} from \"./page_search_model\";\nimport {registry} from '@web/core/registry';\nimport {listView} from '@web/views/list/list_view';\nimport {ConfirmationDialog} from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport {DeletePageDialog, DuplicatePageDialog} from '@website/components/dialog/page_properties';\nimport {CheckboxItem} from \"@web/core/dropdown/checkbox_item\";\n\n\nexport class PageListController extends PageControllerMixin(listView.Controller) {\n    static template = `website.PageListView`;\n    static components = {\n        ...listView.Controller.components,\n        CheckboxItem,\n    };\n\n    /**\n     * @override\n     */\n    setup() {\n        super.setup();\n        if (this.props.resModel === \"website.page\") {\n            this.archiveEnabled = false;\n        }\n    }\n\n    /**\n     * @override\n     */\n    onClickCreate() {\n        return this.createWebsiteContent();\n    }\n\n    /**\n     * Adds a \"Publish/Unpublish\" button to the 'action' menu of the list view.\n     *\n     * @override\n     */\n    getStaticActionMenuItems() {\n        const menuItems = super.getStaticActionMenuItems();\n        if (this.props.fields.hasOwnProperty('is_published')) {\n            menuItems.publish = {\n                sequence: 15,\n                icon: \"fa fa-globe\",\n                description: _t(\"Publish\"),\n                callback: async () => {\n                    this.dialogService.add(ConfirmationDialog, {\n                        title: _t(\"Publish Website Content\"),\n                        body: _t(\"%s record(s) selected, are you sure you want to publish them all?\", this.model.root.selection.length),\n                        confirm: () => this.togglePublished(true),\n                    });\n                },\n            };\n            menuItems.unpublish = {\n                sequence: 16,\n                icon: \"fa fa-chain-broken\",\n                description: _t(\"Unpublish\"),\n                callback: async () => this.togglePublished(false),\n            };\n        }\n        if (this.props.resModel === \"website.page\") {\n            menuItems.duplicate.callback = async (records = []) => {\n                const resIds = this.model.root.selection.map((record) => record.resId);\n                this.dialog.add(DuplicatePageDialog, {\n                    pageIds: resIds,\n                    onDuplicate: () => {\n                        this.model.load();\n                    },\n                });\n            };\n        }\n        return menuItems;\n    }\n\n    async onDeleteSelectedRecords() {\n        const pageIds = this.model.root.selection.map((record) => record.resId);\n        const newPageTemplateRecords = await this.orm.read(\"website.page\", pageIds, [\"is_new_page_template\"]);\n        this.dialogService.add(DeletePageDialog, {\n            resIds: pageIds,\n            resModel: this.props.resModel,\n            onDelete: () => {\n                this.model.root.deleteRecords();\n            },\n            hasNewPageTemplate: newPageTemplateRecords.some(record => record.is_new_page_template),\n        });\n    }\n\n    async togglePublished(publish) {\n        const resIds = this.model.root.selection.map(record => record.resId);\n        await this.orm.write(this.props.resModel, resIds, {is_published: publish});\n        this.actionService.switchView('list');\n    }\n}\n\nexport class PageListRenderer extends listView.Renderer {\n    static recordRowTemplate = \"website.PageListRenderer.RecordRow\";\n}\n\nexport const PageListView = {\n    ...listView,\n    Renderer: PageListRenderer,\n    Controller: PageListController,\n    SearchModel: PageSearchModel,\n};\n\nregistry.category(\"views\").add(\"website_pages_list\", PageListView);\n", "/** @odoo-module */\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Domain } from '@web/core/domain';\nimport { SearchModel } from '@web/search/search_model';\nimport { onWillStart, useState } from \"@odoo/owl\";\n\nexport class PageSearchModel extends SearchModel {\n    /**\n     * @override\n     */\n    setup() {\n        super.setup(...arguments);\n        this.website = useService('website');\n\n        this.pagesState = useState({\n            websiteDomain: false,\n        });\n        onWillStart(async () => {\n            // Before the searchModel performs its DB search call, append the\n            // website domain to the search domain.\n            await this.website.fetchWebsites();\n            const website = await this.getCurrentWebsite();\n            await this.notifyWebsiteChange(website.id);\n        });\n    }\n\n    /**\n     * @override\n     */\n    exportState() {\n        const state = super.exportState();\n        state.websiteDomain = this.pagesState.websiteDomain;\n        return state;\n    }\n\n    /**\n     * @override\n     */\n    _importState(state) {\n        super._importState(...arguments);\n\n        if (state.websiteDomain) {\n            this.pagesState.websiteDomain = state.websiteDomain;\n        }\n    }\n\n    /**\n     * @override\n     */\n    _getDomain(params = {}) {\n        let domain = super._getDomain(params);\n        if (!this.pagesState.websiteDomain) {\n            return domain;\n        }\n\n        domain = Domain.and([\n            domain,\n            this.pagesState.websiteDomain,\n        ]);\n        return params.raw ? domain : domain.toList();\n    }\n\n    /**\n     * Updates the website domain state and notifies the change. That domain\n     * state will be appended to the base SearchModel domain.\n     *\n     * @param {number} websiteId - The ID of the website.\n     */\n    async notifyWebsiteChange(websiteId) {\n        let websiteDomain = [];\n        if (websiteId && 'website_id' in this.searchViewFields) {\n            if (this.resModel === 'website.page') {\n                // In case of `website.page`, we can't find the website pages\n                // with a regular domain (because we need to filter duplicates).\n                const pageIds = await this.orm.call(\n                    \"website\",\n                    \"get_website_page_ids\",\n                    [websiteId],\n                );\n                websiteDomain = [['id', 'in', pageIds]];\n            } else {\n                websiteDomain = [['website_id', 'in', [false, websiteId]]];\n            }\n        }\n        this.pagesState.websiteDomain = websiteDomain;\n        this._notify();\n    }\n\n    /**\n     * Retrieves the current website.\n     *\n     * @returns {Object} The current website.\n     */\n    async getCurrentWebsite() {\n        const currentWebsite = (await this.orm.call('website', 'get_current_website')).match(/\\d+/);\n        if (currentWebsite) {\n            return this.website.websites.find(w => w.id === parseInt(currentWebsite[0]));\n        }\n        return this.website.websites[0];\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 { AddPageDialog } from \"@website/components/dialog/add_page_dialog\";\nimport { onWillStart, useState } from \"@odoo/owl\";\n\n/**\n * Used to share code and keep the same behaviour on different types of 'website\n * content' views:\n * - Trigger the 'new content' dialogs when 'CREATE' button is clicked.\n * - Add a website selector on ControlPanel (that will be used by the renderer\n * to filter content).\n */\nexport const PageControllerMixin = (component) => class extends component {\n    /**\n     * @override\n     */\n    setup() {\n        super.setup();\n        this.website = useService('website');\n        this.dialog = useService('dialog');\n        this.orm = useService('orm');\n\n        this.websiteSelection = odoo.debug ? [{id: 0, name: _t(\"All Websites\")}] : [];\n\n        this.state = useState({\n            activeWebsite: undefined,\n        });\n\n        onWillStart(async () => {\n            // `fetchWebsites()` already done by parent PageSearchModel\n            this.websiteSelection.push(...this.website.websites);\n            this.state.activeWebsite = await this.env.searchModel.getCurrentWebsite();\n        });\n    }\n\n    /**\n     * Adds the new 'website content' record depending on the targeted model and\n     * 'create_action' passed in context.\n     */\n    async createWebsiteContent() {\n        if (this.props.resModel === 'website.page') {\n            return this.dialog.add(AddPageDialog, {\n                websiteId: this.state.activeWebsite.id,\n            });\n        }\n        const action = this.props.context.create_action;\n        if (action) {\n            if (/^\\//.test(action)) {\n                const url = await rpc(action);\n                this.website.goToWebsite({ path: url, edition: true });\n                return;\n            }\n            this.actionService.doAction(action, {\n                onClose: (infos) => {\n                    if (infos) {\n                        this.website.goToWebsite({ path: infos.path });\n                    }\n                },\n                props: {\n                    onSave: (record, params) => {\n                        if (record.resId && params.computePath) {\n                            const path = params.computePath();\n                            this.actionService.doAction({\n                                type: \"ir.actions.act_window_close\",\n                                infos: { path }\n                            });\n                        }\n                    }\n                }\n            });\n        }\n    }\n\n    onSelectWebsite(website) {\n        this.state.activeWebsite = website;\n        this.env.searchModel.notifyWebsiteChange(website.id);\n    }\n};\n", "/** @odoo-module **/\n\nimport { ControlPanel } from \"@web/search/control_panel/control_panel\";\nimport { FormController } from \"@web/views/form/form_controller\";\nimport { formView } from \"@web/views/form/form_view\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { ViewButton } from \"@web/views/view_button/view_button\";\nimport { useSubEnv, onMounted, useEnv } from \"@odoo/owl\";\n\n/*\n* Common code for theme installation/update handler.\n* It overrides the onClickViewButton function that's present in the env.\n* That way, we display our own Loader and make a silent call to the ORM.\n*/\nexport function useLoaderOnClick() {\n    const website = useService('website');\n    const orm = useService('orm');\n    const action = useService('action');\n    const env = useEnv();\n    const previousOnClickViewButton = env.onClickViewButton;\n    useSubEnv({\n        async onClickViewButton(params) {\n            const name = params.clickParams.name;\n            if (['button_refresh_theme', 'button_choose_theme'].includes(name)) {\n                website.invalidateSnippetCache = true;\n                website.showLoader({ showTips: name !== 'button_refresh_theme' });\n                try {\n                    const resParams = params.getResParams();\n                    const callback = await orm.silent.call(resParams.resModel, name, [[resParams.resId]]);\n                    let keepLoader = false;\n                    if (callback) {\n                        callback.target = 'main';\n                        await action.doAction(callback);\n                        if (callback.tag === 'website_preview' && callback.context.params.with_loader) {\n                            keepLoader = true;\n                        }\n                    }\n                    if (!keepLoader) {\n                        website.hideLoader();\n                    }\n                } catch (error) {\n                    website.hideLoader();\n                    throw error;\n                }\n            } else {\n                return previousOnClickViewButton(...arguments);\n            }\n        }\n    });\n}\n\nclass ThemePreviewFormController extends FormController {\n    static components = { ...FormController.components, ViewButton };\n    static template = \"website.ThemePreviewFormController\";\n    /**\n     * @override\n     */\n    setup() {\n        super.setup();\n        useLoaderOnClick();\n\n        // TODO adapt theme previews then remove this\n        onMounted(() => {\n            setTimeout(() => {\n                document.querySelector('button[name=\"button_choose_theme\"]')?.click();\n            }, 0);\n        });\n    }\n    /**\n     * @override\n     */\n    get className() {\n        return {...super.className, 'o_view_form_theme_preview_controller': true};\n    }\n    /**\n     * Handler called when user click on 'Choose another theme' button.\n     */\n    back() {\n        this.env.config.historyBack();\n    }\n}\n\nclass ThemePreviewFormControlPanel extends ControlPanel {\n    static template = \"website.ThemePreviewForm.ControlPanel\";\n    /**\n     * Triggers an event on the main bus.\n     * @see {FieldIframePreview} for the event handler.\n     */\n    onMobileClick() {\n        this.env.bus.trigger('THEME_PREVIEW:SWITCH_MODE', {mode: 'mobile'});\n    }\n    /**\n     * @see {onMobileClick}\n     */\n    onDesktopClick() {\n        this.env.bus.trigger('THEME_PREVIEW:SWITCH_MODE', {mode: 'desktop'});\n    }\n    /**\n     * Handler called when user click on Go Back button.\n     */\n    back() {\n        this.env.config.historyBack();\n    }\n}\n\nconst ThemePreviewFormView = {\n    ...formView,\n    Controller: ThemePreviewFormController,\n    ControlPanel: ThemePreviewFormControlPanel,\n};\n\nregistry.category('views').add('theme_preview_form', ThemePreviewFormView);\n", "/** @odoo-module */\n\nimport { ControlPanel } from \"@web/search/control_panel/control_panel\";\nimport { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { KanbanController } from \"@web/views/kanban/kanban_controller\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useLoaderOnClick } from './theme_preview_form';\nimport { KanbanRecord } from \"@web/views/kanban/kanban_record\";\nimport { KanbanRenderer } from \"@web/views/kanban/kanban_renderer\";\n\nclass ThemePreviewKanbanController extends KanbanController {\n    /**\n     * @override\n     */\n    setup() {\n        super.setup();\n        useLoaderOnClick();\n    }\n}\n\nclass ThemePreviewControlPanel extends ControlPanel {\n    static template = \"website.ThemePreviewKanban.ControlPanel\";\n    setup() {\n        super.setup();\n        this.website = useService('website');\n    }\n    close() {\n        this.website.goToWebsite();\n    }\n\n    get display() {\n        return {\n            layoutActions: false,\n            ...this.props.display,\n        };\n    }\n}\nclass ThemePreviewKanbanrecord extends KanbanRecord {\n\n    /** @override **/\n    getRecordClasses() {\n        return super.getRecordClasses() + \" p-0 border-0 bg-transparent\";\n    }\n}\n\nexport class ThemePreviewKanbanRenderer extends KanbanRenderer {\n    static components = {\n        ...KanbanRenderer.components,\n        KanbanRecord: ThemePreviewKanbanrecord,\n    };\n}\n\nconst ThemePreviewKanbanView = {\n    ...kanbanView,\n    Controller: ThemePreviewKanbanController,\n    ControlPanel: ThemePreviewControlPanel,\n    Renderer: ThemePreviewKanbanRenderer,\n    display: {\n        controlPanel: {},\n    },\n};\n\n\n\nregistry.category('views').add('theme_preview_kanban', ThemePreviewKanbanView);\n", "import { jsToPyLocale } from \"@web/core/l10n/utils\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from '@web/core/registry';\nimport { user } from \"@web/core/user\";\nimport { loadBundle } from \"@web/core/assets\";\nimport { ensureJQuery } from \"@web/core/ensure_jquery\";\nimport { isVisible } from \"@web/core/utils/ui\";\n\nimport { FullscreenIndication } from '../components/fullscreen_indication/fullscreen_indication';\nimport { WebsiteLoader } from '../components/website_loader/website_loader';\nimport { reactive, EventBus } from \"@odoo/owl\";\n\nconst websiteSystrayRegistry = registry.category('website_systray');\n\nexport const unslugHtmlDataObject = (repr) => {\n    const 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\nconst ANONYMOUS_PROCESS_ID = 'ANONYMOUS_PROCESS_ID';\n\nexport const websiteService = {\n    dependencies: ['orm', 'action', 'hotkey'],\n    async start(env, { orm, action, hotkey }) {\n        let websites = [];\n        let currentWebsiteId;\n        let currentMetadata = {};\n        let fullscreen;\n        let pageDocument;\n        let contentWindow;\n        let lastUrl;\n        let websiteRootInstance;\n        let isRestrictedEditor;\n        let isDesigner;\n        let hasMultiWebsites;\n        let actionJsId;\n        let blockingProcesses = [];\n        let modelNamesProm = null;\n        const modelNames = {};\n        let invalidateSnippetCache = false;\n        let lastWebsiteId = null;\n\n        const context = reactive({\n            showNewContentModal: false,\n            showResourceEditor: false,\n            edition: false,\n            isPublicRootReady: false,\n            snippetsLoaded: false,\n            isMobile: false,\n        });\n        const bus = new EventBus();\n\n        hotkey.add(\"escape\", () => {\n            // Toggle fullscreen mode when pressing escape.\n            if (\n                (!currentWebsiteId && !fullscreen)\n                || (pageDocument && isVisible(pageDocument.querySelector(\".modal\")))\n            ) {\n                // Only allow to use this feature while on the website app, or\n                // while it is already fullscreen (in case you left the website\n                // app in fullscreen mode, thanks to CTRL-K), or if a modal\n                // is open within the preview and could be closed with escape.\n                return;\n            }\n            fullscreen = !fullscreen;\n            document.body.classList.toggle('o_website_fullscreen', fullscreen);\n            bus.trigger(fullscreen ? 'FULLSCREEN-INDICATION-SHOW' : 'FULLSCREEN-INDICATION-HIDE');\n        }, { global: true });\n        registry.category('main_components').add('FullscreenIndication', {\n            Component: FullscreenIndication,\n            props: { bus },\n        });\n        registry.category('main_components').add('WebsiteLoader', {\n            Component: WebsiteLoader,\n            props: { bus },\n        });\n        return {\n            set currentWebsiteId(id) {\n                if (id && id !== lastWebsiteId) {\n                    invalidateSnippetCache = true;\n                    lastWebsiteId = id;\n                }\n                currentWebsiteId = id;\n                websiteSystrayRegistry.trigger('EDIT-WEBSITE');\n            },\n            /**\n             * This represents the current website being edited in the\n             * WebsitePreview client action. Multiple components based their\n             * visibility on this value, which is falsy if the client action is\n             * not displayed.\n             */\n            get currentWebsite() {\n                const currentWebsite = websites.find(w => w.id === currentWebsiteId);\n                if (currentWebsite) {\n                    currentWebsite.metadata = currentMetadata;\n                }\n                return currentWebsite;\n            },\n            get websites() {\n                return websites;\n            },\n            get context() {\n                return context;\n            },\n            get bus() {\n                return bus;\n            },\n            set pageDocument(document) {\n                pageDocument = document;\n                if (!document) {\n                    currentMetadata = {};\n                    contentWindow = null;\n                    return;\n                }\n                const { dataset } = document.documentElement;\n                // XML files have no dataset on Firefox, and an empty one on\n                // Chrome.\n                const isWebsitePage = dataset && dataset.websiteId;\n                if (!isWebsitePage) {\n                    currentMetadata = {};\n                } else {\n                    const { mainObject, seoObject, isPublished, canOptimizeSeo, canPublish, editableInBackend, translatable, viewXmlid, defaultLangName, langName } = dataset;\n                    // We ignore multiple menus with the same `content_menu_id`\n                    // in the DOM, since it's possible to have different\n                    // templates for the same content menu (E.g. used for a\n                    // different desktop / mobile UI).\n                    const contentMenus = [\n                        ...new Map(\n                            [...document.querySelectorAll(\"[data-content_menu_id]\")].map(\n                                (menuEl) => [\n                                    menuEl.dataset.content_menu_id,\n                                    [menuEl.dataset.menu_name, menuEl.dataset.content_menu_id],\n                                ]\n                            )\n                        ).values(),\n                    ];\n                    currentMetadata = {\n                        path: document.location.href,\n                        mainObject: unslugHtmlDataObject(mainObject),\n                        seoObject: unslugHtmlDataObject(seoObject),\n                        isPublished: isPublished === 'True',\n                        canOptimizeSeo: canOptimizeSeo === 'True',\n                        canPublish: canPublish === 'True',\n                        editableInBackend: editableInBackend === 'True',\n                        title: document.title,\n                        translatable: !!translatable,\n                        contentMenus,\n                        // TODO: Find a better way to figure out if\n                        // a page is editable or not. For now, we use\n                        // the editable selector because it's the common\n                        // denominator of editable pages.\n                        editable: !!document.getElementById('wrapwrap'),\n                        viewXmlid: viewXmlid,\n                        lang: jsToPyLocale(document.documentElement.getAttribute(\"lang\")),\n                        defaultLangName: defaultLangName,\n                        langName: langName,\n                        direction: document.documentElement.querySelector('#wrapwrap.o_rtl') ? 'rtl' : 'ltr',\n                    };\n                }\n                contentWindow = document.defaultView;\n                websiteSystrayRegistry.trigger('CONTENT-UPDATED');\n            },\n            get pageDocument() {\n                return pageDocument;\n            },\n            get contentWindow() {\n                return contentWindow;\n            },\n            get websiteRootInstance() {\n                return websiteRootInstance;\n            },\n            set websiteRootInstance(rootInstance) {\n                websiteRootInstance = rootInstance;\n                context.isPublicRootReady = !!rootInstance;\n            },\n            set lastUrl(url) {\n                lastUrl = url;\n            },\n            get lastUrl() {\n                return lastUrl;\n            },\n            get isRestrictedEditor() {\n                return isRestrictedEditor === true;\n            },\n            get isDesigner() {\n                return isDesigner === true;\n            },\n            get hasMultiWebsites() {\n                return hasMultiWebsites === true;\n            },\n            get actionJsId() {\n                return actionJsId;\n            },\n            set actionJsId(jsId) {\n                actionJsId = jsId;\n            },\n            get invalidateSnippetCache() {\n                return invalidateSnippetCache;\n            },\n            set invalidateSnippetCache(value) {\n                invalidateSnippetCache = value;\n            },\n\n            goToWebsite({ websiteId, path, edition, translation, lang } = {}) {\n                this.websiteRootInstance = undefined;\n                if (lang) {\n                    invalidateSnippetCache = true;\n                    path = `/website/lang/${encodeURIComponent(lang)}?r=${encodeURIComponent(path)}`;\n                }\n                action.doAction('website.website_preview', {\n                    clearBreadcrumbs: true,\n                    additionalContext: {\n                        params: {\n                            website_id: websiteId || currentWebsiteId,\n                            path: path || (contentWindow && contentWindow.location.href) || '/',\n                            enable_editor: edition,\n                            edit_translations: translation,\n                        },\n                    },\n                });\n            },\n            async fetchUserGroups() {\n                // Fetch user groups, before fetching the websites.\n                [isRestrictedEditor, isDesigner, hasMultiWebsites] = await Promise.all([\n                    user.hasGroup('website.group_website_restricted_editor'),\n                    user.hasGroup('website.group_website_designer'),\n                    user.hasGroup('website.group_multi_website'),\n                ]);\n            },\n            async fetchWebsites() {\n                websites = [...(await orm.searchRead('website', [], ['domain', 'id', 'name']))];\n            },\n            async loadWysiwyg() {\n                await ensureJQuery();\n                await loadBundle('website.backend_assets_all_wysiwyg');\n            },\n            blockPreview(showLoader, processId) {\n                if (!blockingProcesses.length) {\n                    bus.trigger('BLOCK', { showLoader });\n                }\n                blockingProcesses.push(processId || ANONYMOUS_PROCESS_ID);\n            },\n            unblockPreview(processId) {\n                const processIndex = blockingProcesses.indexOf(processId || ANONYMOUS_PROCESS_ID);\n                if (processIndex > -1) {\n                    blockingProcesses.splice(processIndex, 1);\n                    if (blockingProcesses.length === 0) {\n                        bus.trigger('UNBLOCK');\n                    }\n                }\n            },\n            showLoader(props) {\n                bus.trigger('SHOW-WEBSITE-LOADER', props);\n            },\n            hideLoader() {\n                bus.trigger('HIDE-WEBSITE-LOADER');\n            },\n            prepareOutLoader() {\n                bus.trigger(\"PREPARE-OUT-WEBSITE-LOADER\");\n            },\n            /**\n             * Returns the (translated) \"functional\" name of a model\n             * (_description) given its \"technical\" name (_name).\n             *\n             * @param {string} [model]\n             * @returns {string}\n             */\n            async getUserModelName(model = this.currentWebsite.metadata.mainObject.model) {\n                if (!modelNamesProm) {\n                    // FIXME the `get_available_models` is to be removed/changed\n                    // in a near future. This code is to be adapted, probably\n                    // with another helper to map a model functional name from\n                    // its technical map without the need of the right access\n                    // rights (which is why I cannot use search_read here).\n                    modelNamesProm = orm.call(\"ir.model\", \"get_available_models\")\n                        .then(modelsData => {\n                            for (const modelData of modelsData) {\n                                modelNames[modelData['model']] = modelData['display_name'];\n                            }\n                        })\n                        // Precaution in case the util is simply removed without\n                        // adapting this method: not critical, we can restore\n                        // later and use the fallback until the fix is made.\n                        .catch(() => {});\n                }\n                await modelNamesProm;\n                return modelNames[model] || _t(\"Data\");\n            },\n        };\n    },\n};\n\nregistry.category('services').add('website', websiteService);\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            const closeNotification = 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                            closeNotification();\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 option\n     * @return {boolean}\n     * @private\n     */\n    _isCategory(option) {\n        return !!option?.separator;\n    }\n\n    getOption(indices) {\n        const [sourceIndex, optionIndex] = indices;\n        return this.sources[sourceIndex]?.options[optionIndex];\n    }\n\n    /**\n     * @override\n     */\n    onOptionMouseEnter(indices) {\n        if (!this._isCategory(this.getOption(indices))) {\n            return super.onOptionMouseEnter(...arguments);\n        }\n    }\n\n    /**\n     * @override\n     */\n    onOptionMouseLeave(indices) {\n        if (!this._isCategory(this.getOption(indices))) {\n            return super.onOptionMouseLeave(...arguments);\n        }\n    }\n    isActiveSourceOption(indices) {\n        if (!this._isCategory(this.getOption(indices))) {\n            return super.isActiveSourceOption(...arguments);\n        }\n    }\n    /**\n     * @override\n     */\n    selectOption(option) {\n        if (!this._isCategory(option)) {\n            const { value } = Object.getPrototypeOf(option);\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 { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { Component, xml } from \"@odoo/owl\";\n\nexport class BarcodeHandlerField extends Component {\n    static template = xml``;\n    static props = { ...standardFieldProps };\n    setup() {\n        const barcode = useService(\"barcode\");\n        useBus(barcode.bus, \"barcode_scanned\", this.onBarcodeScanned);\n    }\n    onBarcodeScanned(event) {\n        const { barcode } = event.detail;\n        this.props.record.update({ [this.props.name]: barcode });\n    }\n}\n\nexport const barcodeHandlerField = {\n    component: BarcodeHandlerField,\n};\n\nregistry.category(\"fields\").add(\"barcode_handler\", barcodeHandlerField);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { getVisibleElements } from \"@web/core/utils/ui\";\nimport { Macro } from \"@web/core/macro\";\nimport { click, edit } from \"@odoo/hoot-dom\";\n\nfunction clickOnButton(selector) {\n    const button = document.body.querySelector(selector);\n    if (button) {\n        button.click();\n    }\n}\nfunction updatePager(position) {\n    const pager = document.body.querySelector(\"nav.o_pager\");\n    if (!pager || pager.innerText.includes(\"-\")) {\n        // we don't change pages if we are in a multi record view\n        return;\n    }\n    let next;\n    if (position === \"first\") {\n        next = 1;\n    } else {\n        next = parseInt(pager.querySelector(\".o_pager_limit\").textContent, 10);\n    }\n    let current = parseInt(pager.innerText.split('/')[0], 10);\n    if (current === next) {\n        return;\n    }\n    new Macro({\n        checkDelay: 16,\n        name: \"updating pager\",\n        timeout: 1000,\n        steps: [\n            {\n                trigger: \"span.o_pager_value\",\n                async action(trigger) {\n                    await click(trigger);\n                },\n            },\n            {\n                trigger: \"input.o_pager_value\",\n                async action(trigger) {\n                    await click(trigger);\n                    await edit(next, { confirm: \"blur\" });\n                },\n            },\n        ],\n    }).start();\n}\n\nexport const COMMANDS = {\n    \"OCDEDIT\": () => clickOnButton(\".o_form_button_edit\"),\n    \"OCDDISC\": () => clickOnButton(\".o_form_button_cancel\"),\n    \"OCDSAVE\": () => clickOnButton(\".o_form_button_save\"),\n    \"OCDPREV\": () => clickOnButton(\".o_pager_previous\"),\n    \"OCDNEXT\": () => clickOnButton(\".o_pager_next\"),\n    \"OCDPAGERFIRST\": () => updatePager(\"first\"),\n    \"OCDPAGERLAST\": () => updatePager(\"last\"),\n};\n\nexport const barcodeGenericHandlers = {\n    dependencies: [\"ui\", \"barcode\", \"notification\"],\n    start(env, { ui, barcode, notification }) {\n\n        barcode.bus.addEventListener(\"barcode_scanned\", (ev) => {\n            const barcode = ev.detail.barcode;\n            if (barcode.startsWith(\"OBT\")) {\n                let targets = [];\n                try {\n                    // the scanned barcode could be anything, and could crash the queryselectorall\n                    // function\n                    targets = getVisibleElements(ui.activeElement, `[barcode_trigger=${barcode.slice(3)}]`);\n                } catch {\n                    console.warn(`Barcode '${barcode}' is not valid`);\n                }\n                for (let elem of targets) {\n                    elem.click();\n                }\n            }\n            if (barcode.startsWith(\"OCD\")) {\n                const fn = COMMANDS[barcode];\n                if (fn) {\n                    fn();\n                } else {\n                    notification.add(_t(\"Barcode: %(barcode)s\", { barcode }), {\n                        title: _t(\"Unknown barcode command\"),\n                        type: \"danger\"\n                    });\n                }\n            }\n        });\n    }\n};\n\nregistry.category(\"services\").add(\"barcode_handlers\", barcodeGenericHandlers);\n", "/** @odoo-module **/\n\nimport { isBrowserChrome, isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { registry } from \"@web/core/registry\";\nimport { session } from \"@web/session\";\nimport { EventBus, whenReady } from \"@odoo/owl\";\n\nfunction isEditable(element) {\n    return element.matches('input,textarea,[contenteditable=\"true\"]');\n}\n\nfunction makeBarcodeInput() {\n    const inputEl = document.createElement('input');\n    inputEl.setAttribute(\"style\", \"position:fixed;top:50%;transform:translateY(-50%);z-index:-1;opacity:0\");\n    inputEl.setAttribute(\"autocomplete\", \"off\");\n    inputEl.setAttribute(\"inputmode\", \"none\"); // magic! prevent native keyboard from popping\n    inputEl.classList.add(\"o-barcode-input\");\n    inputEl.setAttribute('name', 'barcode');\n    return inputEl;\n}\n\nexport const barcodeService = {\n    // Keys from a barcode scanner are usually processed as quick as possible,\n    // but some scanners can use an intercharacter delay (we support <= 50 ms)\n    maxTimeBetweenKeysInMs: session.max_time_between_keys_in_ms || 150,\n\n    // this is done here to make it easily mockable in mobile tests\n    isMobileChrome: isMobileOS() && isBrowserChrome(),\n\n    cleanBarcode: function(barcode) {\n        return barcode.replace(/Alt|Shift|Control/g, '');\n    },\n\n    start() {\n        const bus = new EventBus();\n        let timeout = null;\n\n        let bufferedBarcode = \"\";\n        let currentTarget = null;\n        let barcodeInput = null;\n\n        function handleBarcode(barcode, target) {\n            bus.trigger('barcode_scanned', {barcode,target});\n            if (target.getAttribute('barcode_events') === \"true\") {\n                const barcodeScannedEvent = new CustomEvent(\"barcode_scanned\", { detail: { barcode, target } });\n                target.dispatchEvent(barcodeScannedEvent);\n            }\n        }\n\n        /**\n         * check if we have a barcode, and trigger appropriate events\n         */\n        function checkBarcode(ev) {\n            let str = barcodeInput ? barcodeInput.value : bufferedBarcode;\n            str = barcodeService.cleanBarcode(str);\n            if (str.length >= 3) {\n                if (ev) {\n                    ev.preventDefault();\n                }\n                handleBarcode(str, currentTarget);\n            }\n            if (barcodeInput) {\n                barcodeInput.value = \"\";\n            }\n            bufferedBarcode = \"\";\n            currentTarget = null;\n        }\n\n        function keydownHandler(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            // Ignore 'Shift', 'Escape', 'Backspace', 'Insert', 'Delete', 'Home', 'End', Arrow*, F*, Page*, ...\n            // meta is often used for UX purpose (like shortcuts)\n            // Notes:\n            // - shiftKey is not ignored because it can be used by some barcode scanner for digits.\n            // - altKey/ctrlKey are not ignored because it can be used in some barcodes (e.g. GS1 separator)\n            const isSpecialKey = !['Control', 'Alt'].includes(ev.key) && (ev.key.length > 1 || ev.metaKey);\n            const isEndCharacter = ev.key.match(/(Enter|Tab)/);\n\n            // Don't catch non-printable keys except 'enter' and 'tab'\n            if (isSpecialKey && !isEndCharacter) {\n                return;\n            }\n\n            currentTarget = ev.target;\n            // Don't catch events targeting elements that are editable because we\n            // have no way of redispatching 'genuine' key events. Resent events\n            // don't trigger native event handlers of elements. So this means that\n            // our fake events will not appear in eg. an <input> element.\n            if (currentTarget !== barcodeInput && isEditable(currentTarget) &&\n                !currentTarget.dataset.enableBarcode &&\n                currentTarget.getAttribute(\"barcode_events\") !== \"true\") {\n                return;\n            }\n\n            clearTimeout(timeout);\n            if (isEndCharacter) {\n                checkBarcode(ev);\n            } else {\n                bufferedBarcode += ev.key;\n                timeout = setTimeout(checkBarcode, barcodeService.maxTimeBetweenKeysInMs);\n            }\n        }\n\n        function mobileChromeHandler(ev) {\n            if (ev.key === \"Unidentified\") {\n                return;\n            }\n            if (document.activeElement && !document.activeElement.matches('input:not([type]), input[type=\"text\"], textarea, [contenteditable], ' +\n                '[type=\"email\"], [type=\"number\"], [type=\"password\"], [type=\"tel\"], [type=\"search\"]')) {\n                barcodeInput.focus();\n            }\n            keydownHandler(ev);\n        }\n\n        whenReady(() => {\n            const isMobileChrome = barcodeService.isMobileChrome;\n            if (isMobileChrome) {\n                barcodeInput = makeBarcodeInput();\n                document.body.appendChild(barcodeInput);\n            }\n            const handler = isMobileChrome ? mobileChromeHandler : keydownHandler;\n            document.body.addEventListener('keydown', handler);\n        });\n\n        return {\n            bus,\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"barcode\", barcodeService);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { scanBarcode } from \"@web/core/barcode/barcode_dialog\";\nimport { isBarcodeScannerSupported } from \"@web/core/barcode/barcode_video_scanner\";\nimport { Component } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class BarcodeScanner extends Component {\n    static template = \"barcodes.BarcodeScanner\";\n    static props = {\n        onBarcodeScanned: { type: Function },\n    };\n\n    setup() {\n        this.notification = useService(\"notification\");\n        this.isBarcodeScannerSupported = isBarcodeScannerSupported();\n        this.scanBarcode = () => scanBarcode(this.env, this.facingMode);\n    }\n\n    get facingMode() {\n        return \"environment\";\n    }\n\n    async openMobileScanner() {\n        let error = null;\n        let barcode = null;\n        try {\n            barcode = await this.scanBarcode();\n        } catch (err) {\n            error = err.message;\n        }\n\n        if (barcode) {\n            this.props.onBarcodeScanned(barcode);\n            if (\"vibrate\" in window.navigator) {\n                window.navigator.vibrate(100);\n            }\n        } else {\n            this.notification.add(error || _t(\"Please, Scan again!\"), {\n                type: \"warning\",\n            });\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { FloatField, floatField } from \"@web/views/fields/float/float_field\";\n\nexport class FloatScannableField extends FloatField {\n    static template = \"barcodes.FloatScannableField\";\n    onBarcodeScanned() {\n        this.inputRef.el.dispatchEvent(new InputEvent(\"input\"));\n    }\n}\n\nexport const floatScannableField = {\n    ...floatField,\n    component: FloatScannableField,\n};\n\nregistry.category(\"fields\").add(\"field_float_scannable\", floatScannableField);\n", "/** @odoo-module **/\n\nexport class BarcodeParser {\n    static barcodeNomenclatureFields = [\"name\", \"rule_ids\", \"upc_ean_conv\"];\n    static barcodeRuleFields = [\"name\", \"sequence\", \"type\", \"encoding\", \"pattern\", \"alias\"];\n    static async fetchNomenclature(orm, id) {\n        const [nomenclature] = await orm.read(\n            \"barcode.nomenclature\",\n            [id],\n            this.barcodeNomenclatureFields\n        );\n        let rules = await orm.searchRead(\n            \"barcode.rule\",\n            [[\"barcode_nomenclature_id\", \"=\", id]],\n            this.barcodeRuleFields\n        );\n        rules = rules.sort((a, b) => {\n            return a.sequence - b.sequence;\n        });\n        nomenclature.rules = rules;\n        return nomenclature;\n    }\n\n    constructor() {\n        this.setup(...arguments);\n    }\n\n    setup({ nomenclature }) {\n        this.nomenclature = nomenclature;\n    }\n\n    /**\n     * This algorithm is identical for all fixed length numeric GS1 data structures.\n     *\n     * It is also valid for EAN-8, EAN-12 (UPC-A), EAN-13 check digit after sanitizing.\n     * https://www.gs1.org/sites/default/files/docs/barcodes/GS1_General_Specifications.pdf\n     *\n     * @param {String} numericBarcode Need to have a length of 18\n     * @returns {number} Check Digit\n     */\n    get_barcode_check_digit(numericBarcode) {\n        let oddsum = 0, evensum = 0, total = 0;\n        // Reverses the barcode to be sure each digit will be in the right place\n        // regardless the barcode length.\n        const code = numericBarcode.split('').reverse();\n        // Removes the last barcode digit (should not be took in account for its own computing).\n        code.shift();\n\n        // Multiply value of each position by\n        // N1  N2  N3  N4  N5  N6  N7  N8  N9  N10 N11 N12 N13 N14 N15 N16 N17 N18\n        // x3  X1  x3  x1  x3  x1  x3  x1  x3  x1  x3  x1  x3  x1  x3  x1  x3  CHECK_DIGIT\n        for (let i = 0; i < code.length; i++) {\n            if (i % 2 === 0) {\n                evensum += parseInt(code[i]);\n            } else {\n                oddsum += parseInt(code[i]);\n            }\n        }\n        total = evensum * 3 + oddsum;\n        return (10 - total % 10) % 10;\n    }\n\n    /**\n     * Checks if the barcode string is encoded with the provided encoding.\n     *\n     * @param {String} barcode\n     * @param {String} encoding could be 'any' (no encoding rules), 'ean8', 'upca' or 'ean13'\n     * @returns {boolean}\n     */\n    check_encoding(barcode, encoding) {\n        if (encoding === 'any') {\n            return true;\n        }\n        const barcodeSizes = {\n            ean8: 8,\n            ean13: 13,\n            upca: 12,\n        };\n        return barcode.length === barcodeSizes[encoding] && /^\\d+$/.test(barcode) &&\n            this.get_barcode_check_digit(barcode) === parseInt(barcode[barcode.length - 1]);\n    }\n\n    /**\n     * Sanitizes a EAN-13 prefix by padding it with chars zero.\n     *\n     * @param {String} ean\n     * @returns {String}\n     */\n    sanitize_ean(ean) {\n        ean = ean.substr(0, 13);\n        ean = \"0\".repeat(13 - ean.length) + ean;\n        return ean.substr(0, 12) + this.get_barcode_check_digit(ean);\n    }\n\n    /**\n     * Sanitizes a UPC-A prefix by padding it with chars zero.\n     *\n     * @param {String} upc\n     * @returns {String}\n     */\n    sanitize_upc(upc) {\n        return this.sanitize_ean(upc).substr(1, 12);\n    }\n\n    // Checks if barcode matches the pattern\n    // Additionnaly retrieves the optional numerical content in barcode\n    // Returns an object containing:\n    // - value: the numerical value encoded in the barcode (0 if no value encoded)\n    // - base_code: the barcode in which numerical content is replaced by 0's\n    // - match: boolean\n    match_pattern(barcode, pattern, encoding) {\n        var match = {\n            value: 0,\n            base_code: barcode,\n            match: false,\n        };\n        barcode = barcode.replace(\"\\\\\", \"\\\\\\\\\").replace(\"{\", '\\{').replace(\"}\", \"\\}\").replace(\".\", \"\\.\");\n\n        var numerical_content = pattern.match(/[{][N]*[D]*[}]/); // look for numerical content in pattern\n        var base_pattern = pattern;\n        if(numerical_content){ // the pattern encodes a numerical content\n            var num_start = numerical_content.index; // start index of numerical content\n            var num_length = numerical_content[0].length; // length of numerical content\n            var value_string = barcode.substr(num_start, num_length-2); // numerical content in barcode\n            var whole_part_match = numerical_content[0].match(\"[{][N]*[D}]\"); // looks for whole part of numerical content\n            var decimal_part_match = numerical_content[0].match(\"[{N][D]*[}]\"); // looks for decimal part\n            var whole_part = value_string.substr(0, whole_part_match.index+whole_part_match[0].length-2); // retrieve whole part of numerical content in barcode\n            var decimal_part = \"0.\" + value_string.substr(decimal_part_match.index, decimal_part_match[0].length-1); // retrieve decimal part\n            if (whole_part === ''){\n                whole_part = '0';\n            }\n            match.value = parseInt(whole_part) + parseFloat(decimal_part);\n\n            // replace numerical content by 0's in barcode and pattern\n            match.base_code = barcode.substr(0, num_start);\n            base_pattern = pattern.substr(0, num_start);\n            for(var i=0;i<(num_length-2);i++) {\n                match.base_code += \"0\";\n                base_pattern += \"0\";\n            }\n            match.base_code += barcode.substr(num_start + num_length - 2, barcode.length - 1);\n            base_pattern += pattern.substr(num_start + num_length, pattern.length - 1);\n\n            match.base_code = match.base_code\n                .replace(\"\\\\\\\\\", \"\\\\\")\n                .replace(\"\\{\", \"{\")\n                .replace(\"\\}\",\"}\")\n                .replace(\"\\.\",\".\");\n\n            var base_code = match.base_code.split('');\n            if (encoding === 'ean13') {\n                base_code[12] = '' + this.get_barcode_check_digit(match.base_code);\n            } else if (encoding === 'ean8') {\n                base_code[7]  = '' + this.get_barcode_check_digit(match.base_code);\n            } else if (encoding === 'upca') {\n                base_code[11] = '' + this.get_barcode_check_digit(match.base_code);\n            }\n            match.base_code = base_code.join('');\n        }\n\n        base_pattern = base_pattern.split('|')\n            .map(part => part.startsWith('^') ? part : '^' + part)\n            .join('|');\n        match.match = match.base_code.match(base_pattern);\n\n        return match;\n    }\n\n    /**\n     * Attempts to interpret a barcode (string encoding a barcode Code-128)\n     *\n     * @param {string} barcode\n     * @returns {Object} the returned object containing informations about the barcode:\n     *      - code: the barcode\n     *      - type: the type of the barcode (e.g. alias, unit product, weighted product...)\n     *      - value: if the barcode encodes a numerical value, it will be put there\n     *      - base_code: the barcode with all the encoding parts set to zero; the one put on the product in the backend\n     */\n    parse_barcode(barcode) {\n        if (barcode.match(/^urn:/)) {\n            return this.parseURI(barcode);\n        }\n        return this.parseBarcodeNomenclature(barcode);\n    }\n\n    parseBarcodeNomenclature(barcode) {\n        const parsed_result = {\n            encoding: '',\n            type:'error',\n            code:barcode,\n            base_code: barcode,\n            value: 0,\n        };\n\n        if (!this.nomenclature) {\n            return parsed_result;\n        }\n\n        var rules = this.nomenclature.rules;\n        for (var i = 0; i < rules.length; i++) {\n            var rule = rules[i];\n            var cur_barcode = barcode;\n\n            if (    rule.encoding === 'ean13' &&\n                    this.check_encoding(barcode,'upca') &&\n                    this.nomenclature.upc_ean_conv in {'upc2ean':'','always':''} ){\n                cur_barcode = '0' + cur_barcode;\n            } else if (rule.encoding === 'upca' &&\n                    this.check_encoding(barcode,'ean13') &&\n                    barcode[0] === '0' &&\n                    this.nomenclature.upc_ean_conv in {'ean2upc':'','always':''} ){\n                cur_barcode = cur_barcode.substr(1,12);\n            }\n\n            if (!this.check_encoding(cur_barcode,rule.encoding)) {\n                continue;\n            }\n\n            var match = this.match_pattern(cur_barcode, rules[i].pattern, rule.encoding);\n            if (match.match) {\n                if(rules[i].type === 'alias') {\n                    barcode = rules[i].alias;\n                    parsed_result.code = barcode;\n                    parsed_result.type = 'alias';\n                }\n                else {\n                    parsed_result.encoding  = rules[i].encoding;\n                    parsed_result.type      = rules[i].type;\n                    parsed_result.value     = match.value;\n                    parsed_result.code      = cur_barcode;\n                    if (rules[i].encoding === \"ean13\"){\n                        parsed_result.base_code = this.sanitize_ean(match.base_code);\n                    }\n                    else{\n                        parsed_result.base_code = match.base_code;\n                    }\n                    return parsed_result;\n                }\n            }\n        }\n        return parsed_result;\n    }\n\n    // URI methods\n    /**\n     * Parse an URI into an object with either the product and its lot/serial\n     * number, either the package.\n     * @param {String} barcode\n     * @returns {Object}\n     */\n    parseURI(barcode) {\n        const uriParts = barcode.split(\":\").map(v => v.trim());\n        // URI should be formatted like that (number is the index once split):\n        // 0: urn, 1: epc, 2: id/tag, 3: identifier, 4: data\n        const identifier = uriParts[3];\n        const data = uriParts[4].split(\".\");\n        if (identifier === \"lgtin\" || identifier === \"sgtin\") {\n            return this.convertURIGTINDataIntoProductAndTrackingNumber(barcode, data);\n        } else if (identifier === \"sgtin-96\" || identifier === \"sgtin-198\") {\n            // Same compute then SGTIN but we have to remove the filter.\n            return this.convertURIGTINDataIntoProductAndTrackingNumber(barcode, data.slice(1));\n        } else if (identifier === \"sscc\") {\n            return this.convertURISSCCDataIntoPackage(barcode, data);\n        } else if (identifier === \"sscc-96\") {\n            // Same compute then SSCC but we have to remove the filter.\n            return this.convertURISSCCDataIntoPackage(barcode, data.slice(1));\n        }\n        return barcode;\n    }\n\n    convertURIGTINDataIntoProductAndTrackingNumber(base_code, data) {\n        const [gs1CompanyPrefix, itemRefAndIndicator, trackingNumber] = data;\n        const indicator = itemRefAndIndicator[0];\n        const itemRef = itemRefAndIndicator.slice(1);\n        let productBarcode = indicator + gs1CompanyPrefix + itemRef;\n        productBarcode += this.get_barcode_check_digit(productBarcode + \"0\");\n        return [\n            {\n                base_code,\n                code: productBarcode,\n                string_value: productBarcode,\n                type: \"product\",\n                value: productBarcode,\n            }, {\n                base_code,\n                code: trackingNumber,\n                string_value: trackingNumber,\n                type: \"lot\",\n                value: trackingNumber,\n            }\n        ];\n    }\n\n    convertURISSCCDataIntoPackage(base_code, data) {\n        const [gs1CompanyPrefix, serialReference] = data;\n        const extension = serialReference[0];\n        const serialRef = serialReference.slice(1);\n        let sscc = extension + gs1CompanyPrefix + serialRef;\n        sscc += this.get_barcode_check_digit(sscc + \"0\");\n        return [{\n            base_code,\n            code: sscc,\n            string_value: sscc,\n            type: \"package\",\n            value: sscc,\n        }];\n    }\n}\n", "/** @odoo-module **/\n\nimport { patch } from \"@web/core/utils/patch\";\nimport { BarcodeParser } from \"@barcodes/js/barcode_parser\";\nimport { _t } from \"@web/core/l10n/translation\";\nexport class GS1BarcodeError extends Error {};\n\nexport const FNC1_CHAR = String.fromCharCode(29);\n\npatch(BarcodeParser, {\n    barcodeNomenclatureFields: [\n        ...BarcodeParser.barcodeNomenclatureFields,\n        \"is_gs1_nomenclature\",\n        \"gs1_separator_fnc1\",\n    ],\n    barcodeRuleFields: [\n        ...BarcodeParser.barcodeRuleFields,\n        \"gs1_content_type\",\n        \"gs1_decimal_usage\",\n        \"associated_uom_id\",\n    ],\n});\n\npatch(BarcodeParser.prototype, {\n    setup(attributes) {\n        super.setup(...arguments);\n        // Use the nomenclature's separaor regex, else use an impossible one.\n        const nomenclatureSeparator = this.nomenclature && this.nomenclature.gs1_separator_fnc1;\n        this.gs1SeparatorRegex = new RegExp(nomenclatureSeparator || '.^', 'g');\n    },\n\n    /**\n     * Convert YYMMDD GS1 date into a Date object\n     *\n     * @param {string} gs1Date YYMMDD string date, length must be 6\n     * @returns {Date}\n     */\n    gs1_date_to_date(gs1Date) {\n        // See 7.12 Determination of century in dates:\n        // https://www.gs1.org/sites/default/files/docs/barcodes/GS1_General_Specifications.pdfDetermination of century\n        const now = new Date();\n        const substractYear = parseInt(gs1Date.slice(0, 2)) - (now.getFullYear() % 100);\n        let century = Math.floor(now.getFullYear() / 100);\n        if (51 <= substractYear && substractYear <= 99) {\n            century--;\n        } else if (-99 <= substractYear && substractYear <= -50) {\n            century++;\n        }\n        const year = century * 100 + parseInt(gs1Date.slice(0, 2));\n        const date = new Date(year, parseInt(gs1Date.slice(2, 4) - 1));\n\n        if (gs1Date.slice(-2) === '00'){\n            // Day is not mandatory, when not set -> last day of the month\n            date.setDate(new Date(year, parseInt(gs1Date.slice(2, 4)), 0).getDate());\n        } else {\n            date.setDate(parseInt(gs1Date.slice(-2)));\n        }\n        return date;\n    },\n\n    /**\n     * Perform interpretation of the barcode value depending of the rule.gs1_content_type\n     *\n     * @param {Array} match Result of a regex match with atmost 2 groups (ia and value)\n     * @param {Object} rule Matched Barcode Rule\n     * @returns {Object|null}\n     */\n    parse_gs1_rule_pattern(match, rule) {\n        const result = {\n            rule: Object.assign({}, rule),\n            ai: match[1],\n            string_value: match[2],\n            code: match[2],\n            base_code: match[2],\n            type: rule.type\n        };\n        if (rule.gs1_content_type === 'measure'){\n            let decimalPosition = 0; // Decimal position begin at the end, 0 means no decimal\n            if (rule.gs1_decimal_usage){\n                decimalPosition = parseInt(match[1][match[1].length - 1]);\n            }\n            if (decimalPosition > 0) {\n                const integral = match[2].slice(0, match[2].length - decimalPosition);\n                const decimal = match[2].slice(match[2].length - decimalPosition);\n                result.value = parseFloat( integral + \".\" + decimal);\n            } else {\n                result.value = parseInt(match[2]);\n            }\n        } else if (rule.gs1_content_type === 'identifier'){\n            if (parseInt(match[2][match[2].length - 1]) !== this.get_barcode_check_digit(\"0\".repeat(18 - match[2].length) + match[2])){\n                throw new Error(_t(\"Invalid barcode: the check digit is incorrect\"));\n                // return {error: _t(\"Invalid barcode: the check digit is incorrect\")};\n            }\n            result.value = match[2];\n        } else if (rule.gs1_content_type === 'date'){\n            if (match[2].length !== 6){\n                throw new Error(_t(\"Invalid barcode: can't be formated as date\"));\n                // return {error: _t(\"Invalid barcode: can't be formated as date\")};\n            }\n            result.value = this.gs1_date_to_date(match[2]);\n        } else {\n            result.value = match[2];\n        }\n        return result;\n    },\n\n    /**\n     * Try to decompose the gs1 extanded barcode into several unit of information using gs1 rules.\n     *\n     * @param {string} barcode\n     * @returns {Array} Array of object\n     */\n    gs1_decompose_extanded(barcode) {\n        const results = [];\n        const rules = this.nomenclature.rules.filter(rule => rule.encoding === 'gs1-128');\n        const separatorReg = `(?:${FNC1_CHAR}+)?`;\n        barcode = this._convertGS1Separators(barcode);\n        barcode = this.cleanBarcode(barcode);\n\n        while (barcode.length > 0) {\n            const barcodeLength = barcode.length;\n            for (const rule of rules) {\n                const match = barcode.match(\"^\" + rule.pattern + separatorReg);\n                if (match && match.length >= 3) {\n                    const res = this.parse_gs1_rule_pattern(match, rule);\n                    if (res) {\n                        barcode = barcode.slice(match.index + match[0].length);\n                        results.push(res);\n                        if (barcode.length === 0) {\n                            return results; // Barcode completly parsed, no need to keep looping.\n                        }\n                    } else {\n                        throw new GS1BarcodeError(_t(\"This barcode can't be parsed by any barcode rules.\"));\n                    }\n                }\n            }\n            if (barcodeLength === barcode.length) {\n                throw new GS1BarcodeError(_t(\"This barcode can't be partially or fully parsed.\"));\n            }\n        }\n\n        return results;\n    },\n\n    /**\n     * @override\n     * @returns {Object|Array|null} If nomenclature is GS1, returns an array or null\n     */\n    parseBarcodeNomenclature(barcode) {\n        if (this.nomenclature && this.nomenclature.is_gs1_nomenclature) {\n            return this.gs1_decompose_extanded(barcode);\n        }\n        return super.parseBarcodeNomenclature(...arguments);\n    },\n\n    /**\n     * Makes all needed operations to clean and prepare the barcode.\n     * @param {string} barcode\n     * @returns {string}\n     */\n    cleanBarcode(barcode) {\n        if (barcode[0] === FNC1_CHAR) {\n            // If first character is the separator, remove it to be able to parse the barcode.\n            barcode = barcode.slice(1);\n        }\n        return barcode;\n    },\n\n    /**\n     * The FNC1 is the default GS1 separator character, but through the field `gs1_separator_fnc1`,\n     * the user has the possibility to define one or multiple characters to use as separator as\n     * a regex. This method replaces all of the matches in the given barcode by the FNC1.\n     *\n     * @param {string} barcode\n     * @returns {string}\n     */\n    _convertGS1Separators: function (barcode) {\n        barcode = barcode.replace(this.gs1SeparatorRegex, FNC1_CHAR);\n        return barcode;\n    },\n});\n", "/** @odoo-module **/\n\nimport { session } from \"@web/session\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { barcodeService } from '@barcodes/barcode_service';\n\nimport { FNC1_CHAR } from \"@barcodes_gs1_nomenclature/js/barcode_parser\";\n\n\npatch(barcodeService, {\n    // Use the regex given by the session, else use an impossible one\n    gs1SeparatorRegex: new RegExp(session.gs1_group_separator_encodings || '.^', 'g'),\n\n    cleanBarcode(barcode) {\n        barcode = barcode.replace(barcodeService.gs1SeparatorRegex, FNC1_CHAR);\n        return super.cleanBarcode(barcode);\n    },\n});\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { sprintf } from \"@web/core/utils/strings\";\n\nasync function doMultiPrint(env, action) {\n    for (const report of action.params.reports) {\n        if (report.type != \"ir.actions.report\") {\n            env.services.notification.add(_t(\"Incorrect type of action submitted as a report, skipping action\"), {\n                title: _t(\"Report Printing Error\"),\n            });\n            continue\n        } else if (report.report_type === \"qweb-html\") {\n            env.services.notification.add(\n                sprintf(\n                    _t(\"HTML reports cannot be auto-printed, skipping report: %s\"),\n                            report.name)\n                , {\n                title: _t(\"Report Printing Error\"),\n            });\n            continue\n        }\n        // WARNING: potential issue if pdf generation fails, then action_service defaults\n        // to HTML and rest of the action chain will break w/potentially never resolving promise\n        await env.services.action.doAction({ type: \"ir.actions.report\", ...report });\n    }\n    if (action.params.anotherAction) {\n        return env.services.action.doAction(action.params.anotherAction);\n    } else if (action.params.onClose) {\n        // handle special cases such as barcode\n        action.params.onClose()\n    } else {\n        return env.services.action.doAction(\"reload_context\");\n    }\n}\n\nregistry.category(\"actions\").add(\"do_multi_print\", doMultiPrint);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Component, onWillStart, useState } from \"@odoo/owl\";\nimport { download } from \"@web/core/network/download\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useSetupAction } from \"@web/search/action_hook\";\nimport { Layout } from \"@web/search/layout\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\n\nfunction processLine(line) {\n    return { ...line, lines: [], isFolded: true };\n}\n\nfunction extractPrintData(lines) {\n    const data = [];\n    for (const line of lines) {\n        const { id, model_id, model, unfoldable, level } = line;\n        data.push({\n            id: id,\n            model_id: model_id,\n            model_name: model,\n            unfoldable,\n            level: level || 1,\n        });\n        if (!line.isFolded) {\n            data.push(...extractPrintData(line.lines));\n        }\n    }\n    return data;\n}\n\nexport class TraceabilityReport extends Component {\n    static template = \"stock.TraceabilityReport\";\n    static components = { Layout };\n    static props = { ...standardActionServiceProps };\n\n    setup() {\n        this.actionService = useService(\"action\");\n        this.orm = useService(\"orm\");\n\n        onWillStart(this.onWillStart);\n        useSetupAction({\n            getLocalState: () => ({\n                lines: [...this.state.lines],\n            }),\n        });\n\n        this.state = useState({\n            lines: this.props.state?.lines || [],\n        });\n\n        const { active_id, active_model, auto_unfold, context, lot_name, ttype, url, lang } =\n            this.props.action.context;\n        this.controllerUrl = url;\n\n        this.context = context || {};\n        Object.assign(this.context, {\n            active_id: active_id || this.props.action.params.active_id,\n            auto_unfold: auto_unfold || false,\n            model: active_model || this.props.action.context.params?.active_model || false,\n            lot_name: lot_name || false,\n            ttype: ttype || false,\n            lang: lang || false,\n        });\n\n        if (this.context.model) {\n            this.props.updateActionState({ active_model: this.context.model });\n        }\n\n        this.display = {\n            controlPanel: {},\n            searchPanel: false,\n        };\n    }\n\n    async onWillStart() {\n        if (!this.state.lines.length) {\n            const mainLines = await this.orm.call(\"stock.traceability.report\", \"get_main_lines\", [\n                this.context,\n            ]);\n            this.state.lines = mainLines.map(processLine);\n        }\n    }\n\n    onClickBoundLink(line) {\n        this.actionService.doAction({\n            type: \"ir.actions.act_window\",\n            res_model: line.res_model,\n            res_id: line.res_id,\n            views: [[false, \"form\"]],\n            target: \"current\",\n        });\n    }\n\n    onCLickOpenLot(line) {\n        this.actionService.doAction({\n            type: \"ir.actions.client\",\n            tag: \"stock_report_generic\",\n            name: line.lot_name !== undefined && line.lot_name.toString(),\n            context: {\n                active_id: line.lot_id,\n                active_model: \"stock.lot\",\n                url: \"/stock/output_format/stock?active_id=:active_id&active_model=:active_model\",\n            },\n        });\n    }\n\n    onClickUpDownStream(line) {\n        this.actionService.doAction({\n            type: \"ir.actions.client\",\n            tag: \"stock_report_generic\",\n            name: _t(\"Traceability Report\"),\n            context: {\n                active_id: line.model_id,\n                active_model: line.model,\n                auto_unfold: true,\n                lot_name: line.lot_name !== undefined && line.lot_name,\n                url: \"/stock/output_format/stock/active_id\",\n            },\n        });\n    }\n\n    onClickPrint() {\n        const data = JSON.stringify(extractPrintData(this.state.lines));\n        const url = this.controllerUrl\n            .replace(\":active_id\", this.context.active_id)\n            .replace(\":active_model\", this.context.model)\n            .replace(\"output_format\", \"pdf\");\n\n        download({\n            data: { data },\n            url,\n        });\n    }\n\n    async toggleLine(line) {\n        line.isFolded = !line.isFolded;\n        if (!line.lines.length) {\n            line.lines = (\n                await this.orm.call(\"stock.traceability.report\", \"get_lines\", [line.id], {\n                    model_id: line.model_id,\n                    model_name: line.model,\n                    level: line.level + 30 || 1,\n                })\n            ).map(processLine);\n        }\n    }\n}\n\nregistry.category(\"actions\").add(\"stock_report_generic\", TraceabilityReport);\n", "/** @odoo-module **/\nimport { useService } from \"@web/core/utils/hooks\";\nimport { formatFloat } from \"@web/views/fields/formatters\";\nimport { Component } from \"@odoo/owl\";\n\nexport class ReceptionReportLine extends Component {\n    static template = \"stock.ReceptionReportLine\";\n    static props = {\n        data: Object,\n        parentIndex: String,\n        showUom: Boolean,\n        precision: Number,\n    };\n\n    setup() {\n        this.ormService = useService(\"orm\");\n        this.actionService = useService(\"action\");\n        this.formatFloat = (val) => formatFloat(val, { digits: [false, this.props.precision] });\n    }\n\n    //---- Handlers ----\n\n    async onClickForecast() {\n        const action = await this.ormService.call(\n            \"stock.move\",\n            \"action_product_forecast_report\",\n            [[this.data.move_out_id]],\n        );\n\n        return this.actionService.doAction(action);\n    }\n\n    async onClickPrint() {\n        if (!this.data.move_out_id) {\n            return;\n        }\n        const reportFile = 'stock.report_reception_report_label';\n        const modelIds = [this.data.move_out_id];\n        const productQtys = [Math.ceil(this.data.quantity) || '1'];\n\n        return this.actionService.doAction({\n            type: \"ir.actions.report\",\n            report_type: \"qweb-pdf\",\n            report_name: `${reportFile}?docids=${modelIds}&quantity=${productQtys}`,\n            report_file: reportFile,\n        });\n    }\n\n    async onClickAssign() {\n        await this.ormService.call(\n            \"report.stock.report_reception\",\n            \"action_assign\",\n            [false, [this.data.move_out_id], [this.data.quantity], [this.data.move_ins]],\n        );\n        this.env.bus.trigger(\"update-assign-state\", { isAssigned: true, tableIndex: this.props.parentIndex, lineIndex: this.data.index });\n    }\n\n    async onClickUnassign() {\n        const done = await this.ormService.call(\n            \"report.stock.report_reception\",\n            \"action_unassign\",\n            [false, this.data.move_out_id, this.data.quantity, this.data.move_ins]\n        )\n        if (done) {\n            this.env.bus.trigger(\"update-assign-state\", { isAssigned: false, tableIndex: this.props.parentIndex, lineIndex: this.data.index });\n        }\n    }\n\n    //---- Getters ----\n\n    get data() {\n        return this.props.data;\n    }\n}\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { ControlPanel } from \"@web/search/control_panel/control_panel\";\nimport { ReceptionReportTable } from \"../reception_report_table/stock_reception_report_table\";\nimport { Component, onWillStart, useState } from \"@odoo/owl\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\n\nexport class ReceptionReportMain extends Component {\n    static template = \"stock.ReceptionReportMain\";\n    static components = {\n        ControlPanel,\n        ReceptionReportTable,\n    };\n    static props = { ...standardActionServiceProps };\n\n    setup() {\n        this.controlPanelDisplay = {};\n        this.ormService = useService(\"orm\");\n        this.actionService = useService(\"action\");\n        this.reportName = \"stock.report_reception\";\n        this.state = useState({\n            sourcesToLines: {},\n        });\n        useBus(this.env.bus, \"update-assign-state\", (ev) => this._changeAssignedState(ev.detail));\n\n        onWillStart(async () => {\n            // Check the URL if report was alreadu loaded.\n            let defaultDocIds;\n            const { rfield, rids } = this.props.action.context.params || {};\n            if (rfield && rids) {\n                const parsedIds = JSON.parse(rids);\n                defaultDocIds = [rfield, parsedIds instanceof Array ? parsedIds : [parsedIds]];\n            } else {\n                defaultDocIds = Object.entries(this.context).find(([k,v]) => k.startsWith(\"default_\"));\n                if (!defaultDocIds) {\n                    // If nothing could be found, just ask for empty data.\n                    defaultDocIds = [false, [0]];\n                }\n            }\n            this.contextDefaultDoc = { field: defaultDocIds[0], ids: defaultDocIds[1] };\n\n            if (this.contextDefaultDoc.field) {\n                // Add the fields/ids to the URL, so we can properly reload them after a page refresh.\n                this.props.updateActionState({ rfield: this.contextDefaultDoc.field, rids: JSON.stringify(this.contextDefaultDoc.ids) });\n            }\n            this.data = await this.getReportData();\n            this.state.sourcesToLines = this.data.sources_to_lines;\n        });\n    }\n\n    async getReportData() {\n        const context = { ...this.context, [this.contextDefaultDoc.field]: this.contextDefaultDoc.ids };\n        const args = [\n            this.contextDefaultDoc.ids,\n            { context, report_type: \"html\" },\n        ];\n        return this.ormService.call(\n            \"report.stock.report_reception\",\n            \"get_report_data\",\n            args,\n            { context },\n        );\n    }\n\n    //---- Handlers ----\n\n    async onClickAssignAll() {\n        const moveIds = [];\n        const quantities = [];\n        const inIds = [];\n\n        for (const lines of Object.values(this.state.sourcesToLines)) {\n            for (const line of lines) {\n                if (line.is_assigned) continue;\n                moveIds.push(line.move_out_id);\n                quantities.push(line.quantity);\n                inIds.push(line.move_ins);\n            }\n        }\n\n        await this.ormService.call(\n            \"report.stock.report_reception\",\n            \"action_assign\",\n            [false, moveIds, quantities, inIds],\n        );\n        this._changeAssignedState({ isAssigned: true });\n    }\n\n    async onClickTitle(docId) {\n        return this.actionService.doAction({\n            type: \"ir.actions.act_window\",\n            res_model: this.data.doc_model,\n            res_id: docId,\n            views: [[false, \"form\"]],\n            target: \"current\",\n        });\n    }\n\n    onClickPrint() {\n        return this.actionService.doAction({\n            type: \"ir.actions.report\",\n            report_type: \"qweb-pdf\",\n            report_name: `${this.reportName}/?context={\"${this.contextDefaultDoc.field}\": ${JSON.stringify(this.contextDefaultDoc.ids)}}`,\n            report_file: this.reportName,\n        });\n    }\n\n    onClickPrintLabels() {\n        const reportFile = 'stock.report_reception_report_label';\n        const modelIds = [];\n        const quantities = [];\n        \n        for (const lines of Object.values(this.state.sourcesToLines)) {\n            for (const line of lines) {\n                if (!line.is_assigned) continue;\n                modelIds.push(line.move_out_id);\n                quantities.push(Math.ceil(line.quantity) || 1);\n            }\n        }\n        if (!modelIds.length) {\n            return;\n        }\n\n        return this.actionService.doAction({\n            type: \"ir.actions.report\",\n            report_type: \"qweb-pdf\",\n            report_name: `${reportFile}?docids=${modelIds}&quantity=${quantities}`,\n            report_file: reportFile,\n        });\n    }\n\n    //---- Utils ----\n\n    _changeAssignedState(options) {\n        const { isAssigned, tableIndex, lineIndex } = options;\n\n        for (const [tabIndex, lines] of Object.entries(this.state.sourcesToLines)) {\n            if (tableIndex && tableIndex != tabIndex) continue;\n            lines.forEach(line => {\n                if (isNaN(lineIndex) || lineIndex == line.index) {\n                    line.is_assigned = isAssigned;\n                }\n            });\n        }\n    }\n\n    //---- Getters ----\n\n    get context() {\n        return this.props.action.context;\n    }\n\n    get hasContent() {\n        return this.data.sources_to_lines && Object.keys(this.data.sources_to_lines).length > 0;\n    }\n\n    get isAssignAllDisabled() {\n        return Object.values(this.state.sourcesToLines).every(lines => lines.every(line => line.is_assigned || !line.is_qty_assignable));\n    }\n\n    get isPrintLabelDisabled() {\n        return Object.values(this.state.sourcesToLines).every(lines => lines.every(line => !line.is_assigned));\n    }\n}\n\nregistry.category(\"actions\").add(\"reception_report\", ReceptionReportMain);\n", "/** @odoo-module **/\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { ReceptionReportLine } from \"../reception_report_line/stock_reception_report_line\";\nimport { Component } from \"@odoo/owl\";\n\nexport class ReceptionReportTable extends Component {\n    static template = \"stock.ReceptionReportTable\";\n    static components = {\n        ReceptionReportLine,\n    };\n    static props = {\n        index: String,\n        scheduledDate: { type: String, optional: true },\n        lines: Array,\n        source: Array,\n        showUom: Boolean,\n        precision: Number,\n    };\n\n    setup() {\n        this.actionService = useService(\"action\");\n        this.ormService = useService(\"orm\");\n    }\n\n    //---- Handlers ----\n\n    async onClickAssignAll() {\n        const moveIds = [];\n        const quantities = [];\n        const inIds = [];\n        for (const line of this.props.lines) {\n            if (line.is_assigned) continue;\n            moveIds.push(line.move_out_id);\n            quantities.push(line.quantity);\n            inIds.push(line.move_ins);\n        }\n\n        await this.ormService.call(\n            \"report.stock.report_reception\",\n            \"action_assign\",\n            [false, moveIds, quantities, inIds],\n        );\n        this.env.bus.trigger(\"update-assign-state\", { isAssigned: true, tableIndex: this.props.index });\n    }\n\n    async onClickLink(resModel, resId, viewType) {\n        return this.actionService.doAction({\n            type: \"ir.actions.act_window\",\n            res_model: resModel,\n            res_id: resId,\n            views: [[false, viewType]],\n            target: \"current\",\n        });\n    }\n\n    async onClickPrintLabels() {\n        const reportFile = 'stock.report_reception_report_label';\n        const modelIds = [];\n        const quantities = [];\n        for (const line of this.props.lines) {\n            if (!line.is_assigned) continue;\n            modelIds.push(line.move_out_id);\n            quantities.push(Math.ceil(line.quantity) || 1);\n        }\n        if (!modelIds.length) {\n            return;\n        }\n\n        return this.actionService.doAction({\n            type: \"ir.actions.report\",\n            report_type: \"qweb-pdf\",\n            report_name: `${reportFile}?docids=${modelIds}&quantity=${quantities}`,\n            report_file: reportFile,\n        });\n    }\n\n    //---- Getters ----\n\n    get hasMovesIn() {\n        return this.props.lines.some(line => line.move_ins && line.move_ins.length > 0);\n    }\n\n    get hasAssignAllButton() {\n        return this.props.lines.some(line => line.is_qty_assignable);\n    }\n\n    get isAssignAllDisabled() {\n        return this.props.lines.every(line => line.is_assigned);\n    }\n\n    get isPrintLabelDisabled() {\n        return this.props.lines.every(line => !line.is_assigned);\n    }\n}\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { KanbanRenderer } from \"@web/views/kanban/kanban_renderer\";\n\nimport { DynamicRecordList } from \"@web/model/relational_model/dynamic_record_list\";\nimport { DynamicGroupList } from \"@web/model/relational_model/dynamic_group_list\";\n\nexport class StockKanbanRenderer extends KanbanRenderer {\n    setup() {\n        super.setup();\n    }\n\n    // If all Inventory Overview graphs are empty, we use random sample data\n    getGroupsOrRecords() {\n        const { list } = this.props;\n        let records = [];\n        if (list instanceof DynamicRecordList) {\n            records.push(...list.records);\n        } else if (list instanceof DynamicGroupList) {\n            list.groups.forEach(g => {\n                records.push(...g.list.records);\n            });\n        }\n        // Data type \"sample\" is assigned in Python to empty graph data\n        let allEmpty = records.every(r => {\n            return r.data.kanban_dashboard_graph.includes('\"type\": \"sample\"');\n        });\n        if (allEmpty) {\n            records.forEach(r => {\n                let parsedDashboardData = JSON.parse(r.data.kanban_dashboard_graph);\n                parsedDashboardData[0].values.forEach(d => {\n                    d.value = Math.floor(Math.random() * 9 + 1);\n                });\n                r.data.kanban_dashboard_graph = JSON.stringify(parsedDashboardData);\n            });\n        }\n        return super.getGroupsOrRecords();\n    }\n}\n\nexport const StockKanbanView = {\n    ...kanbanView,\n    Renderer: StockKanbanRenderer,\n};\n\nregistry.category(\"views\").add(\"stock_dashboard_kanban\", StockKanbanView);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { X2ManyField, x2ManyField } from \"@web/views/fields/x2many/x2many_field\";\nimport { useSelectCreate, useOpenMany2XRecord} from \"@web/views/fields/relational_utils\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Domain } from \"@web/core/domain\";\n\nexport class SMLX2ManyField extends X2ManyField {\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.dirtyQuantsData = new Map();\n        const selectCreate = useSelectCreate({\n            resModel: \"stock.quant\",\n            activeActions: this.activeActions,\n            onSelected: (resIds) => this.selectRecord(resIds),\n            onCreateEdit: () => this.createOpenRecord(),\n        });\n\n        this.selectCreate = (params) => {\n            return selectCreate(params);\n        };\n        this.openQuantRecord = useOpenMany2XRecord({\n            resModel: \"stock.quant\",\n            activeActions: this.activeActions,\n            onRecordSaved: (record) => this.selectRecord([record.resId]),\n            fieldString: this.props.string,\n            is2Many: true,\n        });\n    }\n\n    async onAdd({ context, editable } = {}) {\n        if (!this.props.record.data.show_quant) {\n            return super.onAdd(...arguments);\n        }\n        // Compute the quant offset from move lines quantity changes that were not saved yet.\n        // Hence, did not yet affect quant's quantity in DB.\n        await this.updateDirtyQuantsData();\n        context = {\n            ...context,\n            single_product: true,\n            list_view_ref: \"stock.view_stock_quant_tree_simple\",\n            search_default_on_hand: true,\n            search_default_in_stock: true,\n        };\n        const productName = this.props.record.data.product_id[1];\n        const title = _t(\"Add line: %s\", productName);\n        let domain = [\n            [\"product_id\", \"=\", this.props.record.data.product_id[0]],\n            [\"location_id\", \"child_of\", this.props.context.default_location_id],\n        ];\n        if (this.dirtyQuantsData.size) {\n            const notFullyUsed = [];\n            const fullyUsed = [];\n            for (const [quantId, quantData] of this.dirtyQuantsData.entries()) {\n                if (quantData.available_quantity > 0) {\n                    notFullyUsed.push(quantId);\n                } else {\n                    fullyUsed.push(quantId);\n                }\n            }\n            if (fullyUsed.length) {\n                domain = Domain.and([domain, [[\"id\", \"not in\", fullyUsed]]]).toList();\n            }\n            if (notFullyUsed.length) {\n                domain = Domain.or([domain, [[\"id\", \"in\", notFullyUsed]]]).toList();\n            }\n        }\n        return this.selectCreate({ domain, context, title });\n    }\n\n    async updateDirtyQuantsData() {\n        // Since changes of move line quantities will not affect the available quantity of the quant before\n        // the record has been saved, it is necessary to determine the offset of the DB quant data.\n        this.dirtyQuantsData.clear();\n        const dirtyQuantityMoveLines = this.props.record.data.move_line_ids.records.filter(\n            (ml) => !ml.data.quant_id && ml._values.quantity - ml._changes.quantity\n        );\n        const dirtyQuantMoveLines = this.props.record.data.move_line_ids.records.filter(\n            (ml) => ml.data.quant_id[0]\n        );\n        const dirtyMoveLines = [...dirtyQuantityMoveLines, ...dirtyQuantMoveLines];\n        if (!dirtyMoveLines.length) {\n            return;\n        }\n        const match = await this.orm.call(\n            \"stock.move.line\",\n            \"get_move_line_quant_match\",\n            [\n                this.props.record.data.move_line_ids.records\n                    .filter((rec) => rec.resId)\n                    .map((rec) => rec.resId),\n                this.props.record.resId,\n                dirtyMoveLines.filter((rec) => rec.resId).map((rec) => rec.resId),\n                dirtyQuantMoveLines.map((ml) => ml.data.quant_id[0]),\n            ],\n            {}\n        );\n        const quants = match[0];\n        if (!quants.length) {\n            return;\n        }\n        const dbMoveLinesData = new Map();\n        for (const data of match[1]) {\n            dbMoveLinesData.set(data[0], { quantity: data[1].quantity, quantId: data[1].quant_id });\n        }\n        const offsetByQuant = new Map();\n        for (const ml of dirtyQuantMoveLines) {\n            const quantId = ml.data.quant_id[0];\n            offsetByQuant.set(quantId, (offsetByQuant.get(quantId) || 0) - ml.data.quantity);\n            const dbQuantId = dbMoveLinesData.get(ml.resId)?.quantId;\n            if (dbQuantId && quantId != dbQuantId) {\n                offsetByQuant.set(\n                    dbQuantId,\n                    (offsetByQuant.get(dbQuantId) || 0) + dbMoveLinesData.get(ml.resId).quantity\n                );\n            }\n        }\n        const offsetByQuantity = new Map();\n        for (const ml of dirtyQuantityMoveLines) {\n            offsetByQuantity.set(ml.resId, ml._values.quantity - ml._changes.quantity);\n        }\n        for (const quant of quants) {\n            const quantityOffest = quant[1].move_line_ids\n                .map((ml) => offsetByQuantity.get(ml) || 0)\n                .reduce((val, sum) => val + sum, 0);\n            const quantOffest = offsetByQuant.get(quant[0]) || 0;\n            this.dirtyQuantsData.set(quant[0], {\n                available_quantity: quant[1].available_quantity + quantityOffest + quantOffest,\n            });\n        }\n    }\n\n    async selectRecord(res_ids) {\n        const demand =\n            this.props.record.data.product_uom_qty -\n            this.props.record.data.move_line_ids.records\n                .map((ml) => ml.data.quantity)\n                .reduce((val, sum) => val + sum, 0);\n        const params = {\n            context: { default_quant_id: res_ids[0] },\n        };\n        if (demand <= 0) {\n            params.context.default_quantity = 0;\n        } else if (this.dirtyQuantsData.has(res_ids[0])) {\n            params.context.default_quantity = Math.min(\n                this.dirtyQuantsData.get(res_ids[0]).available_quantity,\n                demand\n            );\n        }\n        this.list.addNewRecord(params).then((record) => {\n            // Make it dirty to force the save of the record. addNewRecord make\n            // the new record dirty === False by default to remove them at unfocus event\n            record.dirty = true;\n        });\n    }\n\n    createOpenRecord() {\n        const activeElement = document.activeElement;\n        this.openQuantRecord({\n            context: {\n                ...this.props.context,\n                form_view_ref: \"stock.view_stock_quant_form\",\n            },\n            immediate: true,\n            onClose: () => {\n                if (activeElement) {\n                    activeElement.focus();\n                }\n            },\n        });\n    }\n}\n\nexport const smlX2ManyField = {\n    ...x2ManyField,\n    component: SMLX2ManyField,\n};\n\nregistry.category(\"fields\").add(\"sml_x2_many\", smlX2ManyField);\n", "import { cookie } from \"@web/core/browser/cookie\";\nimport { getColor, getCustomColor } from \"@web/core/colors/colors\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\nimport { JournalDashboardGraphField } from \"@web/views/fields/journal_dashboard_graph/journal_dashboard_graph_field\";\n\nexport class PickingTypeDashboardGraphField extends JournalDashboardGraphField {\n    setup() {\n        super.setup();\n        this.actionService = useService(\"action\");\n    }\n    getBarChartConfig() {\n        // Only bar chart is available for picking types\n        const data = [];\n        const labels = [];\n        const backgroundColor = [];\n\n        const colorPast = getColor(8, cookie.get(\"color_scheme\"));\n        const colorPresent = getColor(16, cookie.get(\"color_scheme\"));\n        const colorFuture = getColor(12, cookie.get(\"color_scheme\"));\n        this.data[0].values.forEach((pt) => {\n            data.push(pt.value);\n            labels.push(pt.label);\n            if (pt.type === \"past\") {\n                backgroundColor.push(colorPast);\n            } else if (pt.type === \"present\") {\n                backgroundColor.push(colorPresent);\n            } else if (pt.type === \"future\") {\n                backgroundColor.push(colorFuture);\n            } else {\n                backgroundColor.push(getCustomColor(cookie.get(\"color_scheme\"), \"#ebebeb\", \"#3C3E4B\"));\n            }\n        });\n        return {\n            type: \"bar\",\n            data: {\n                labels,\n                datasets: [\n                    {\n                        backgroundColor,\n                        data,\n                        fill: \"start\",\n                        label: this.data[0].key,\n                    },\n                ],\n            },\n            options: {\n                onClick: (e) => {\n                    const pickingTypeId = e.chart.config._config.options.pickingTypeId;\n                    // If no picking type ID was provided, than this is sample data\n                    if (!pickingTypeId) {\n                        return;\n                    }\n                    const columnIndex = e.chart.tooltip.dataPoints[0].parsed.x;\n                    const dateCategories = {\n                        0: \"before\",\n                        1: \"yesterday\",\n                        2: \"today\",\n                        3: \"day_1\",\n                        4: \"day_2\",\n                        5: \"after\",\n                    };\n                    const dateCategory = dateCategories[columnIndex];\n                    const additionalContext = {\n                        picking_type_id: pickingTypeId,\n                        search_default_picking_type_id: [pickingTypeId],\n                    };\n                    // Add a filter for the given date category\n                    additionalContext[\"search_default_\".concat(dateCategory)] = true;\n                    this.actionService.doAction(\"stock.click_dashboard_graph\", {\n                        additionalContext: additionalContext\n                    });\n                },\n                plugins: {\n                    legend: { display: false },\n                    tooltip: {\n                        intersect: false,\n                        position: \"nearest\",\n                        caretSize: 0,\n                    },\n                },\n                scales: {\n                    y: {\n                        display: false,\n                    },\n                    x: {\n                        display: false,\n                    },\n                },\n                pickingTypeId: this.data[0].picking_type_id,\n                maintainAspectRatio: false,\n                elements: {\n                    line: {\n                        tension: 0.000001,\n                    },\n                },\n            },\n        };\n    }\n}\n\nexport const pickingTypeDashboardGraphField = {\n    component: PickingTypeDashboardGraphField,\n    supportedTypes: [\"text\"],\n    extractProps: ({ attrs }) => ({\n        graphType: attrs.graph_type,\n    }),\n};\n\nregistry.category(\"fields\").add(\"picking_type_dashboard_graph\", pickingTypeDashboardGraphField);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component } from \"@odoo/owl\";\n\nexport class ForecastedButtons extends Component {\n    static template = \"stock.ForecastedButtons\";\n    static props = {\n        action: Object,\n        resModel: { type: String, optional: true },\n        reloadReport: Function,\n    };\n\n    setup() {\n        this.actionService = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.context = this.props.action.context;\n        this.productId = this.context.active_id;\n        this.resModel = this.props.resModel || this.context.active_model || this.context.params?.active_model || 'product.template';\n    }\n\n    /**\n     * Called when an action open a wizard. If the wizard is discarded, this\n     * method does nothing, otherwise it reloads the report.\n     * @param {Object | undefined} res\n     */\n    _onClose(res) {\n        return res?.special || this.props.reloadReport();\n    }\n\n    async _onClickReplenish() {\n        const context = { ...this.context };\n        if (this.resModel === 'product.product') {\n            context.default_product_id = this.productId;\n        } else if (this.resModel === 'product.template') {\n            context.default_product_tmpl_id = this.productId;\n        }\n        context.default_warehouse_id = this.context.warehouse_id;\n\n        const action = {\n            res_model: 'product.replenish',\n            name: _t('Product Replenish'),\n            type: 'ir.actions.act_window',\n            views: [[false, 'form']],\n            target: 'new',\n            context: context,\n        };\n        return this.actionService.doAction(action, { onClose: this._onClose.bind(this) });\n    }\n\n    async _onClickUpdateQuantity() {\n        const action = await this.orm.call(this.resModel, \"action_update_quantity_on_hand\", [[this.productId]]);\n        if (action.res_model === \"stock.quant\") { // Quant view in inventory mode.\n            action.views = [[false, \"list\"]];\n        }\n        return this.actionService.doAction(action, { onClose: this._onClose.bind(this) });\n    }\n}\n", "/** @odoo-module **/\nimport { formatFloat } from \"@web/views/fields/formatters\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component } from \"@odoo/owl\";\n\nexport class ForecastedDetails extends Component {\n    static template = \"stock.ForecastedDetails\";\n    static props = { docs: Object, openView: Function, reloadReport: Function };\n\n    setup() {\n        this.orm = useService(\"orm\");\n\n        this.onHandCondition =\n            this.props.docs.lines.length &&\n            !this.props.docs.lines.some((line) => line.document_in || line.replenishment_filled);\n\n        this._formatFloat = (num) => {\n            return formatFloat(num, { digits: this.props.docs.precision });\n        };\n    }\n\n    async _reserve(move_id){\n        await this.orm.call(\n            'stock.forecasted_product_product',\n            'action_reserve_linked_picks',\n            [move_id],\n        );\n        this.props.reloadReport();\n    }\n\n    async _unreserve(move_id){\n        await this.orm.call(\n            'stock.forecasted_product_product',\n            'action_unreserve_linked_picks',\n            [move_id],\n        );\n        this.props.reloadReport();\n    }\n\n    async _onClickChangePriority(modelName, record) {\n        const value = record.priority == \"0\" ? \"1\" : \"0\";\n\n        await this.orm.call(modelName, \"write\", [[record.id], { priority: value }]);\n        this.props.reloadReport();\n    }\n\n    displayReserve(line){\n        return !line.in_transit && this.canReserveOperation(line);\n    }\n\n    canReserveOperation(line){\n        return line.move_out?.picking_id;\n    }\n\n    get futureVirtualAvailable() {\n        return this.props.docs.virtual_available + this.props.docs.qty.in - this.props.docs.qty.out;\n    }\n}\n", "/** @odoo-module **/\nimport { useService } from \"@web/core/utils/hooks\";\nimport { formatFloat } from \"@web/views/fields/formatters\";\nimport { Component, markup } from \"@odoo/owl\";\n\nexport class ForecastedHeader extends Component {\n    static template = \"stock.ForecastedHeader\";\n    static props = { docs: Object, openView: Function };\n\n    setup(){\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n\n        this._formatFloat = (num) => formatFloat(num, { digits: this.props.docs.precision });\n    }\n\n    async _onClickInventory(){\n        const context = this._getActionContext();\n        const action = await this.orm.call('stock.quant', 'action_view_quants', [], { context });\n        if (action.help) {\n            action.help = markup(action.help);\n        }\n        return this.action.doAction(action);\n    }\n\n    _getActionContext(){\n        const context = { ...this.context };\n        const templates = this.props.docs.product_templates_ids;\n        if (templates) {\n            context.search_default_product_tmpl_id = templates;\n        } else {\n            context.search_default_product_id = this.props.docs.product_variants_ids;\n        }\n        return context;\n    }\n}\n", "/** @odoo-module **/\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, onWillStart } from \"@odoo/owl\";\n\nexport class ForecastedWarehouseFilter extends Component {\n    static template = \"stock.ForecastedWarehouseFilter\";\n    static components = { Dropdown, DropdownItem };\n    static props = { action: Object, setWarehouseInContext: Function, warehouses: Array };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.context = this.props.action.context;\n        this.warehouses = this.props.warehouses;\n        onWillStart(this.onWillStart)\n    }\n\n    async onWillStart() {\n        this.displayWarehouseFilter = (this.warehouses.length > 1);\n    }\n\n    _onSelected(id){\n        this.props.setWarehouseInContext(Number(id));\n    }\n\n    get activeWarehouse(){\n        let warehouseId = null;\n        if (Array.isArray(this.context.warehouse_id)) {\n            const validWarehouseIds = this.context.warehouse_id.filter(Number.isInteger);\n            warehouseId = validWarehouseIds.length ? validWarehouseIds[0] : null;\n        } else if (Number.isInteger(this.context.warehouse_id)) {\n            warehouseId = this.context.warehouse_id;\n        }\n        return warehouseId ? this.warehouses.find((w) => w.id == warehouseId) : this.warehouses[0];\n    }\n\n    get warehousesItems() {\n        return this.warehouses.map(warehouse => ({\n            id: warehouse.id,\n            label: warehouse.name,\n            onSelected: () => this._onSelected(warehouse.id),\n        }));\n    }\n}\n", "/** @odoo-module **/\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { View } from \"@web/views/view\";\nimport { ControlPanel } from \"@web/search/control_panel/control_panel\";\n\nimport { ForecastedButtons } from \"./forecasted_buttons\";\nimport { ForecastedDetails } from \"./forecasted_details\";\nimport { ForecastedHeader } from \"./forecasted_header\";\nimport { ForecastedWarehouseFilter } from \"./forecasted_warehouse_filter\";\nimport { Component, onWillStart, useState } from \"@odoo/owl\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\n\nexport class StockForecasted extends Component {\n    static template = \"stock.Forecasted\";\n    static components = {\n        ControlPanel,\n        ForecastedButtons,\n        ForecastedWarehouseFilter,\n        ForecastedHeader,\n        View,\n        ForecastedDetails,\n    };\n    static props = { ...standardActionServiceProps };\n    setup() {\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n\n        this.context = useState(this.props.action.context);\n        this.productId = this.context.active_id;\n        this.resModel = this.context.active_model;\n        this.title = this.props.action.name || _t(\"Forecasted Report\");\n        if(!this.context.active_id){\n            this.context.active_id = this.props.action.params.active_id;\n            this.reloadReport();\n        }\n        this.warehouses = useState([]);\n\n        onWillStart(this._getReportValues);\n    }\n\n    async _getReportValues() {\n        await this._getResModel();\n        const isTemplate = !this.resModel || this.resModel === 'product.template';\n        this.reportModelName = `stock.forecasted_product_${isTemplate ? \"template\" : \"product\"}`;\n        this.warehouses.splice(0, this.warehouses.length);\n        this.warehouses.push(...await this.orm.searchRead('stock.warehouse', [],['id', 'name', 'code']));\n        if (!this.context.warehouse_id) {\n            this.updateWarehouse(this.warehouses[0].id);\n        }\n        const reportValues = await this.orm.call(this.reportModelName, \"get_report_values\", [], {\n            context: this.context,\n            docids: [this.productId],\n        });\n        this.docs = {\n            ...reportValues.docs,\n            ...reportValues.precision,\n            lead_days_date: this.context.lead_days_date,\n            qty_to_order: this.context.qty_to_order,\n            visibility_days_date: this.context.visibility_days_date,\n            qty_to_order_with_visibility_days: this.context.qty_to_order_with_visibility_days\n        };\n    }\n\n    async _getResModel(){\n        this.resModel = this.context.active_model || this.context.params?.active_model;\n        //Following is used as a fallback when the forecast is not called by an action but through browser's history\n        if (!this.resModel) {\n            let resModel = this.props.action.res_model;\n            if (resModel) {\n                if (/^\\d+$/.test(resModel)) {\n                    // legacy action definition where res_model is the model id instead of name\n                    const actionModel = await this.orm.read('ir.model', [Number(resModel)], ['model']);\n                    resModel = actionModel[0]?.model;\n                }\n                this.resModel = resModel;\n            } else if (this.props.action._originalAction) {\n                const originalContextAction = JSON.parse(this.props.action._originalAction).context;\n                if (typeof originalContextAction === \"string\") {\n                    this.resModel = JSON.parse(originalContextAction.replace(/'/g, '\"')).active_model;\n                } else if (originalContextAction) {\n                    this.resModel = originalContextAction.active_model;\n                }\n            }\n            this.context.active_model = this.resModel;\n        }\n    }\n\n    async updateWarehouse(id) {\n        const hasPreviousValue = this.context.warehouse_id !== undefined;\n        this.context.warehouse_id = id;\n        if (hasPreviousValue) {\n            await this.reloadReport();\n        }\n    }\n\n    async reloadReport() {\n        const actionRequest = {\n            id: this.props.action.id,\n            type: \"ir.actions.client\",\n            tag: \"stock_forecasted\",\n            context: this.context,\n            name: this.title,\n        };\n        const options = { stackPosition: \"replaceCurrentAction\" };\n        return this.action.doAction(actionRequest, options);\n    }\n\n    get graphDomain() {\n        let warehouseId = null;\n        if (Array.isArray(this.context.warehouse_id)) {\n            const validWarehouseIds = this.context.warehouse_id.filter(Number.isInteger);\n            warehouseId = validWarehouseIds.length ? validWarehouseIds[0] : null;\n        } else if (Number.isInteger(this.context.warehouse_id)) {\n            warehouseId = this.context.warehouse_id;\n        }\n        const domain = [\n            [\"state\", \"=\", \"forecast\"],\n            [\"warehouse_id\", \"=\", warehouseId],\n        ];\n        if (this.resModel === \"product.template\") {\n            domain.push([\"product_tmpl_id\", \"=\", this.productId]);\n        } else if (this.resModel === \"product.product\") {\n            domain.push([\"product_id\", \"=\", this.productId]);\n        }\n        return domain;\n    }\n\n    get graphInfo() {\n        return { noContentHelp: _t(\"Try to add some incoming or outgoing transfers.\") };\n    }\n\n    async openView(resModel, view, resId) {\n        const action = {\n            type: \"ir.actions.act_window\",\n            res_model: resModel,\n            views: [[false, view]],\n            view_mode: view,\n            res_id: resId,\n        };\n        return this.action.doAction(action);\n    }\n}\n\nregistry.category(\"actions\").add(\"stock_forecasted\", StockForecasted);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { UPDATE_METHODS } from \"@web/core/orm_service\";\nimport { rpcBus } from \"@web/core/network/rpc\";\n\nregistry.category(\"services\").add(\"stock_warehouse\", {\n    dependencies: [\"action\"],\n    start(env, { action }) {\n        rpcBus.addEventListener(\"RPC:RESPONSE\", (ev) => {\n            const { data, error } = ev.detail;\n            const { model, method } = data.params;\n            if (!error && model === \"stock.warehouse\") {\n                if (UPDATE_METHODS.includes(method) && !browser.localStorage.getItem(\"running_tour\")) {\n                    action.doAction(\"reload_context\");\n                }\n            }\n        });\n    },\n});\n", "/** @odoo-module **/\n\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { useEffect } from \"@odoo/owl\";\n\nexport class AutoColumnWidthListRenderer extends ListRenderer {\n    static props = [...ListRenderer.props];\n    setup() {\n        super.setup();\n        useEffect(\n            () => {\n                this.keepColumnWidths = false;\n            },\n            () => [this.columns]\n        );\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { DynamicRecordList } from \"@web/model/relational_model/dynamic_record_list\";\nimport { RelationalModel } from \"@web/model/relational_model/relational_model\";\n\nexport class InventoryReportListModel extends RelationalModel {\n    /**\n     * Override\n     */\n    setup(params, { action, dialog, notification, rpc, user, view, company }) {\n        // model has not created any record yet\n        this._lastCreatedRecordId;\n        return super.setup(...arguments);\n    }\n\n    /**\n     * Function called when a record has been _load (after saved).\n     * We need to detect when the user added to the list a quant which already exists\n     * (see stock.quant.create), either already loaded or not, to warn the user\n     * the quant was updated.\n     * This is done by checking :\n     * - the record id against the '_lastCreatedRecordId' on model\n     * - the create_date against the write_date (both are equal for newly created records).\n     *\n     */\n    async _updateSimilarRecords(reloadedRecord, serverValues) {\n        if (this.config.isMonoRecord) {\n            return;\n        }\n\n        const justCreated = reloadedRecord.id == this._lastCreatedRecordId;\n        if (justCreated && serverValues.create_date !== serverValues.write_date) {\n            this.notification.add(\n                _t(\n                    \"You tried to create a record that already exists. The existing record was modified instead.\"\n                ),\n                { title: _t(\"This record already exists.\") }\n            );\n            const duplicateRecords = this.root.records.filter(\n                (record) => record.resId === reloadedRecord.resId && record.id !== reloadedRecord.id\n            );\n            if (duplicateRecords.length > 0) {\n                /* more than 1 'resId' record loaded in view (user added an already loaded record) :\n                 * - both have been updated\n                 * - remove the current record (the added one)\n                 */\n                await this.root._removeRecords([reloadedRecord.id]);\n                for (const record of duplicateRecords) {\n                    record._applyValues(serverValues);\n                }\n            }\n        } else {\n            super._updateSimilarRecords(...arguments)\n        }\n    }\n}\n\nexport class InventoryReportListDynamicRecordList extends DynamicRecordList {\n    /**\n     * Override\n     */\n    async addNewRecord() {\n        const record = await super.addNewRecord(...arguments);\n        // keep created record id on model\n        record.model._lastCreatedRecordId = record.id;\n        return record;\n    }\n}\n\nInventoryReportListModel.DynamicRecordList = InventoryReportListDynamicRecordList;\n", "/** @odoo-module */\n\nimport { listView } from \"@web/views/list/list_view\";\nimport { InventoryReportListModel } from \"./inventory_report_list_model\";\nimport { registry } from \"@web/core/registry\";\n\nexport const InventoryReportListView = {\n    ...listView,\n    Model: InventoryReportListModel,\n};\n\nregistry.category(\"views\").add('inventory_report_list', InventoryReportListView);\n", "/** @odoo-module **/\n\nimport { listView } from '@web/views/list/list_view';\nimport { registry } from \"@web/core/registry\";\nimport { StockReportSearchModel } from \"../search/stock_report_search_model\";\nimport { StockReportSearchPanel } from '../search/stock_report_search_panel';\n\n\nexport const StockReportListView = {\n    ...listView,\n    SearchModel: StockReportSearchModel,\n    SearchPanel: StockReportSearchPanel,\n};\n\nregistry.category(\"views\").add(\"stock_report_list_view\", StockReportListView);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { AutoColumnWidthListRenderer } from \"@stock/views/list/auto_column_width_list_renderer\";\nimport { X2ManyField, x2ManyField } from \"@web/views/fields/x2many/x2many_field\";\n\nexport class MovesListRenderer extends AutoColumnWidthListRenderer {\n    static recordRowTemplate = \"stock.MovesListRenderer.RecordRow\";\n\n    processAllColumn(allColumns, list) {\n        let cols = super.processAllColumn(...arguments);\n        if (list.resModel === \"stock.move\") {\n            cols.push({\n                type: 'opendetailsop',\n                id: `column_detailOp_${cols.length}`,\n                column_invisible: 'parent.state==\"draft\"',\n            });\n        }\n        return cols;\n    }\n}\n\nexport class StockMoveX2ManyField extends X2ManyField {\n    static components = { ...X2ManyField.components, ListRenderer: MovesListRenderer };\n    setup() {\n        super.setup();\n        this.canOpenRecord = true;\n    }\n\n    get isMany2Many() {\n        return false;\n    }\n\n    async openRecord(record) {\n        if (this.canOpenRecord && !record.isNew) {\n            const dirty = await record.isDirty();\n            if (await record._parentRecord.isDirty() || (dirty && 'quantity' in record._changes)) {\n                await record._parentRecord.save({ reload: true });\n                record = record._parentRecord.data[this.props.name].records.find(e => e.resId === record.resId);\n                if (!record) {\n                    return;\n                }\n            }\n        }\n        return super.openRecord(record);\n    }\n}\n\n\nexport const stockMoveX2ManyField = {\n    ...x2ManyField,\n    component: StockMoveX2ManyField,\n    additionalClasses: [...x2ManyField.additionalClasses || [], \"o_field_one2many\"],\n};\n\nregistry.category(\"fields\").add(\"stock_move_one2many\", stockMoveX2ManyField);\n", "/** @odoo-module **/\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { SearchModel } from \"@web/search/search_model\";\nimport { debounce } from \"@web/core/utils/timing\";\n\n\nexport class StockOrderpointSearchModel extends SearchModel {\n    static DEBOUNCE_DELAY = 500;\n\n    setup(services) {\n        super.setup(services);\n        this.ui = useService(\"ui\");\n        this.applyGlobalVisibilityDays = debounce(\n            this.applyGlobalVisibilityDays.bind(this),\n            StockOrderpointSearchModel.DEBOUNCE_DELAY\n        );\n    }\n\n    async applyGlobalVisibilityDays(globalVisibilityDays) {\n        this.ui.block();\n        this.globalContext = {\n            ...this.globalContext,\n            global_visibility_days: globalVisibilityDays,\n        };\n        this._context = false; // Force rebuild of this.context to take into account the updated this.globalContext\n        await this.orm.call(\"stock.warehouse.orderpoint\", \"action_open_orderpoints\", [], {\n            context: this.context,\n        });\n        await this._fetchSections(this.categories, this.filters);\n        this._notify();\n        this.ui.unblock();\n    }\n}\n", "/** @odoo-module **/\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { onWillStart, useState } from '@odoo/owl';\nimport { SearchPanel } from \"@web/search/search_panel/search_panel\";\n\n\nexport class StockOrderpointSearchPanel extends SearchPanel {\n    static template = \"stock.StockOrderpointSearchPanel\";\n\n    setup() {\n        this.orm = useService(\"orm\");\n        super.setup(...arguments);\n        this.globalVisibilityDays = useState({value: 0});\n        this.state.sidebarExpanded = false;\n        onWillStart(this.getVisibilityParameter);\n    }\n\n    async getVisibilityParameter() {\n        let res = await this.orm.call(\"stock.warehouse.orderpoint\", \"get_visibility_days\", []);\n        this.globalVisibilityDays.value = Math.abs(parseInt(res)) || 0;\n    }\n\n    async applyGlobalVisibilityDays(ev) {\n        this.globalVisibilityDays.value = Math.max(parseInt(ev.target.value), 0);\n        await this.env.searchModel.applyGlobalVisibilityDays(this.globalVisibilityDays.value);\n    }\n}\n", "/** @odoo-module **/\n\nimport { SearchModel } from \"@web/search/search_model\";\n\nexport class StockReportSearchModel extends SearchModel {\n\n    async load() {\n        await super.load(...arguments);\n        await this._loadWarehouses();\n      }\n\n\n    //---------------------------------------------------------------------\n    // Actions / Getters\n    //---------------------------------------------------------------------\n\n    getWarehouses() {\n        return this.warehouses;\n    }\n\n    async _loadWarehouses() {\n        this.warehouses = await this.orm.call(\n            'stock.warehouse',\n            'get_current_warehouses',\n            [[]],\n            { context: this.context },\n        );\n    }\n\n    /**\n     * Clears the context of a warehouse so values calculate based on all possible\n     * warehouses\n     */\n    clearWarehouseContext() {\n        delete this.globalContext.warehouse_id;\n        this._notify();\n    }\n\n    /**\n     * @param {number} warehouse_id\n     * Sets the context to the selected warehouse so values that take this into account\n     * will recalculate based on this without filtering out any records\n     */\n    applyWarehouseContext(warehouse_id) {\n        this.globalContext['warehouse_id'] = warehouse_id;\n        this._notify();\n    }\n}\n", "/** @odoo-module **/\n\nimport { SearchPanel } from \"@web/search/search_panel/search_panel\";\n\nexport class StockReportSearchPanel extends SearchPanel {\n    static template = \"stock.StockReportSearchPanel\";\n    setup() {\n        super.setup(...arguments);\n        this.selectedWarehouse = false;\n    }\n\n    //---------------------------------------------------------------------\n    // Actions / Getters\n    //---------------------------------------------------------------------\n\n    get warehouses() {\n        return this.env.searchModel.getWarehouses();\n    }\n\n    clearWarehouseContext() {\n        this.env.searchModel.clearWarehouseContext();\n        this.selectedWarehouse = null;\n    }\n\n    applyWarehouseContext(warehouse_id) {\n        this.env.searchModel.applyWarehouseContext(warehouse_id);\n        this.selectedWarehouse = warehouse_id;\n    }\n}\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { listView } from \"@web/views/list/list_view\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { Component } from \"@odoo/owl\";\nimport { useActionLinks } from \"@web/views/view_hook\";\n\nexport class StockActionHelper extends Component {\n    static template = \"stock.StockActionHelper\";\n    static props = [\"noContentHelp\"];\n    setup() {\n        const resModel = \"searchModel\" in this.env ? this.env.searchModel.resModel : undefined;\n        this.handler = useActionLinks(resModel);\n    }\n}\n\nexport class StockListRenderer extends ListRenderer {\n    static template = \"stock.StockListRenderer\";\n    static components = {\n        ...StockListRenderer.components,\n        StockActionHelper,\n    };\n}\n\nexport const StockListView = {\n    ...listView,\n    Renderer: StockListRenderer,\n};\n\nregistry.category(\"views\").add(\"stock_list_view\", StockListView);\n", "/** @odoo-module */\n\nimport { ListController } from '@web/views/list/list_controller';\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\n\nexport class StockOrderpointListController extends ListController {\n    static template = \"stock.StockOrderpoint.listView\";\n\n    static components = {\n        ...super.components,\n        Dropdown,\n        DropdownItem,\n    }\n\n    async onClickOrder(force_to_max) {\n        const resIds = await this.getSelectedResIds();\n        const action = await this.model.orm.call(this.props.resModel, 'action_replenish', [resIds], {\n            context: this.props.context,\n            force_to_max: force_to_max,\n        });\n        if (action) {\n            await this.actionService.doAction(action);\n        }\n        return this.actionService.doAction('stock.action_replenishment', {\n            stackPosition: 'replaceCurrentAction',\n        });\n    }\n\n    async onClickSnooze() {\n        const resIds = await this.getSelectedResIds();\n        this.actionService.doAction('stock.action_orderpoint_snooze', {\n            additionalContext: { default_orderpoint_ids: resIds },\n            onClose: () => {\n                this.actionService.doAction('stock.action_replenishment', {\n                    stackPosition: 'replaceCurrentAction',\n                });\n            }\n        });\n    }\n}\n", "/** @odoo-module */\n\nimport { listView } from '@web/views/list/list_view';\nimport { registry } from \"@web/core/registry\";\nimport { StockOrderpointListController as Controller } from './stock_orderpoint_list_controller';\nimport { StockOrderpointSearchPanel } from './search/stock_orderpoint_search_panel';\nimport { StockOrderpointSearchModel } from './search/stock_orderpoint_search_model';\n\nexport const StockOrderpointListView = {\n    ...listView,\n    Controller,\n    SearchPanel: StockOrderpointSearchPanel,\n    SearchModel: StockOrderpointSearchModel,\n};\n\nregistry.category(\"views\").add(\"stock_orderpoint_list\", StockOrderpointListView);\n", "/** @odoo-module **/\n\nimport { FloatField, floatField } from \"@web/views/fields/float/float_field\";\nimport { registry } from \"@web/core/registry\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { useEffect, useRef } from \"@odoo/owl\";\n\nexport class CountedQuantityWidgetField extends FloatField {\n    setup() {\n        // Need to adapt useInputField to overide onInput and onChange\n        super.setup();\n\n        const inputRef = useRef(\"numpadDecimal\");\n\n        useEffect(\n            (inputEl) => {\n                if (inputEl) {\n                    inputEl.addEventListener(\"input\", this.onInput.bind(this));\n                    inputEl.addEventListener(\"keydown\", this.onKeydown.bind(this));\n                    inputEl.addEventListener(\"blur\", this.onBlur.bind(this));\n                    return () => {\n                        inputEl.removeEventListener(\"input\", this.onInput.bind(this));\n                        inputEl.removeEventListener(\"keydown\", this.onKeydown.bind(this));\n                        inputEl.removeEventListener(\"blur\", this.onBlur.bind(this));\n                    };\n                }\n            },\n            () => [inputRef.el]\n        );\n    }\n\n    onInput(ev) {\n        return this.props.record.update({ inventory_quantity_set: true });\n    }\n\n    updateValue(ev){\n        try {\n           const val = this.parse(ev.target.value);\n            this.props.record.update({ [this.props.name]: val });\n        } catch {} // ignore since it will be handled later\n    }\n\n    onBlur(ev) {\n         if (!this.props.record.data.inventory_quantity_set) {\n           return;\n        }\n        this.updateValue(ev);\n    }\n\n    onKeydown(ev) {\n        const hotkey = getActiveHotkey(ev);\n        if ([\"enter\", \"tab\", \"shift+tab\"].includes(hotkey)) {\n            this.updateValue(ev);\n            this.onInput(ev);\n        }\n    }\n\n    get formattedValue() {\n        if (\n            this.props.readonly &&\n            !this.props.record.data[this.props.name] & !this.props.record.data.inventory_quantity_set\n        ) {\n            return \"\";\n        }\n        return super.formattedValue;\n    }\n}\n\nexport const countedQuantityWidgetField = {\n    ...floatField,\n    component: CountedQuantityWidgetField,\n};\n\nregistry.category(\"fields\").add(\"counted_quantity_widget\", countedQuantityWidgetField);\n", "/** @odoo-module */\n\nimport { FloatField, floatField } from \"@web/views/fields/float/float_field\";\nimport { formatDate } from \"@web/core/l10n/dates\";\nimport { formatFloat } from \"@web/views/fields/formatters\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class ForecastWidgetField extends FloatField {\n    static template = \"stock.ForecastWidget\";\n    setup() {\n        const { data, fields, resId } = this.props.record;\n        this.actionService = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.resId = resId;\n\n        this.reservedAvailability = formatFloat(data.quantity, {\n            ...fields.quantity,\n            ...this.nodeOptions,\n        });\n        this.forecastExpectedDate = formatDate(\n            data.forecast_expected_date,\n            fields.forecast_expected_date\n        );\n        if (data.forecast_expected_date && data.date_deadline) {\n            this.forecastIsLate = data.forecast_expected_date > data.date_deadline;\n        }\n        const digits = fields.forecast_availability.digits;\n        const options = { digits, thousandsSep: \"\", decimalPoint: \".\" };\n        const forecast_availability = parseFloat(formatFloat(data.forecast_availability, options));\n        const product_qty = parseFloat(formatFloat(data.product_qty, options));\n        this.willBeFulfilled = forecast_availability >= product_qty;\n        this.state = data.state;\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Opens the Forecast Report for the `stock.move` product.\n     */\n    async _openReport(ev) {\n        ev.preventDefault();\n        ev.stopPropagation();\n        if (!this.resId) {\n            return;\n        }\n        const action = await this.orm.call(\"stock.move\", \"action_product_forecast_report\", [\n            this.resId,\n        ]);\n        this.actionService.doAction(action);\n    }\n}\n\nexport const forecastWidgetField = {\n    ...floatField,\n    component: ForecastWidgetField,\n};\n\nregistry.category(\"fields\").add(\"forecast_widget\", forecastWidgetField);\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { x2ManyCommands } from \"@web/core/orm_service\";\nimport { Dialog } from '@web/core/dialog/dialog';\nimport { useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\nimport { parseInteger  } from \"@web/views/fields/parsers\";\nimport { getId } from \"@web/model/relational_model/utils\";\nimport { Component, useRef, onMounted } from \"@odoo/owl\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\n\nexport class GenerateDialog extends Component {\n    static template = \"stock.generate_serial_dialog\";\n    static components = { Dialog };\n    static props = {\n        mode: { type: String },\n        move: { type: Object },\n        close: { type: Function },\n    };\n    setup() {\n        this.size = 'md';\n        if (this.props.mode === 'generate') {\n            this.title = this.props.move.data.has_tracking === 'lot'\n            ? _t(\"Generate Lot numbers\")\n            : _t(\"Generate Serial numbers\");\n        } else {\n            this.title = this.props.move.data.has_tracking === 'lot' ? _t(\"Import Lots\") : _t(\"Import Serials\");\n        }\n\n        this.nextSerial = useRef('nextSerial');\n        this.nextSerialCount = useRef('nextSerialCount');\n        this.totalReceived = useRef('totalReceived');\n        this.keepLines = useRef('keepLines');\n        this.lots = useRef('lots');\n        this.orm = useService(\"orm\");\n        onMounted(() => {\n            if (this.props.mode === 'generate') {\n                this.nextSerialCount.el.value = this.props.move.data.product_uom_qty || 2;\n                if (this.props.move.data.has_tracking === 'lot') {\n                    this.totalReceived.el.value = this.props.move.data.quantity;\n                }\n            }\n        });\n    }\n    async _onGenerate() {\n        let count;\n        let qtyToProcess;\n        if (this.props.move.data.has_tracking === 'lot'){\n            count = parseFloat(this.nextSerialCount.el?.value || '0');\n            qtyToProcess = parseFloat(this.totalReceived.el?.value || this.props.move.data.product_qty);\n        } else {\n            count = parseInteger(this.nextSerialCount.el?.value || '0');\n            qtyToProcess = this.props.move.data.product_qty;\n        }\n        const move_line_vals = await this.orm.call(\"stock.move\", \"action_generate_lot_line_vals\", [{\n                ...this.props.move.context,\n                default_product_id: this.props.move.data.product_id[0],\n                default_location_dest_id: this.props.move.data.location_dest_id[0],\n                default_location_id: this.props.move.data.location_id[0],\n                default_tracking: this.props.move.data.has_tracking,\n                default_quantity: qtyToProcess,\n            },\n            this.props.mode,\n            this.nextSerial.el?.value,\n            count,\n            this.lots.el?.value,\n        ]);\n        const newlines = [];\n        let lines = []\n        lines = this.props.move.data.move_line_ids;\n\n        // create records directly from values to bypass onchanges\n        for (const values of move_line_vals) {\n            newlines.push(\n                lines._createRecordDatapoint(values, {\n                    mode: 'readonly',\n                    virtualId: getId(\"virtual\"),\n                    manuallyAdded: false,\n                })\n            );\n        }\n        if (!this.keepLines.el.checked) {\n            await lines._applyCommands(lines._currentIds.map((currentId) => [\n                x2ManyCommands.DELETE,\n                currentId,\n            ]));\n        }\n        lines.records.push(...newlines);\n        lines._commands.push(...newlines.map((record) => [\n            x2ManyCommands.CREATE,\n            record._virtualId,\n        ]));\n        lines._currentIds.push(...newlines.map((record) => record._virtualId));\n        await lines._onUpdate();\n        this.props.close();\n    }\n}\n\nclass GenerateSerials extends Component {\n    static template = \"stock.GenerateSerials\";\n    static props = {...standardWidgetProps};\n\n    setup(){\n        this.dialog = useService(\"dialog\");\n    }\n\n    openDialog(ev){\n        this.dialog.add(GenerateDialog, {\n            move: this.props.record,\n            mode: 'generate',\n        });\n    }\n}\n\nclass ImportLots extends Component {\n    static template = \"stock.ImportLots\";\n    static props = {...standardWidgetProps};\n    setup(){\n        this.dialog = useService(\"dialog\");\n    }\n\n    openDialog(ev){\n        this.dialog.add(GenerateDialog, {\n            move: this.props.record,\n            mode: 'import',\n        });\n    }\n}\nregistry.category(\"view_widgets\").add(\"import_lots\", {component: ImportLots});\nregistry.category(\"view_widgets\").add(\"generate_serials\", {component: GenerateSerials});\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\nimport { Component, onWillStart } from \"@odoo/owl\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nexport class JsonPopOver extends Component {\n    static template = \"\";\n    static props = {...standardFieldProps};\n    get jsonValue() {\n        return JSON.parse(this.props.record.data[this.props.name]);\n    }\n}\n\nexport const jsonPopOver = {\n    component: JsonPopOver,\n    displayName: _t(\"Json Popup\"),\n    supportedTypes: [\"char\"],\n};\n\nexport class PopOverLeadDays extends JsonPopOver {\n    static template = \"stock.leadDays\";\n    setup() {\n        super.setup();\n        onWillStart(async () => {\n            this.displayUOM = await user.hasGroup(\"uom.group_uom\");\n        });\n    }\n\n    get qtyForecast() {\n        return this._formatQty(\"qty_forecast\");\n    }\n    get qtyToOrder() {\n        return this._formatQty(\"qty_to_order\");\n    }\n    get productMaxQty() {\n        return this._formatQty(\"product_max_qty\");\n    }\n    get productMinQty() {\n        return this._formatQty(\"product_min_qty\");\n    }\n\n    _formatQty(field) {\n        return this.displayUOM\n            ? `${this.jsonValue[field]} ${this.jsonValue.product_uom_name}`\n            : this.jsonValue[field];\n    }\n}\n\n\nexport const popOverLeadDays = {\n    ...jsonPopOver,\n    component: PopOverLeadDays,\n};\nregistry.category(\"fields\").add(\"lead_days_widget\", popOverLeadDays);\n\nexport class ReplenishmentHistoryWidget extends JsonPopOver {\n    static template = \"stock.replenishmentHistory\";\n}\n\nexport const replenishmentHistoryWidget = {\n    ...jsonPopOver,\n    component: ReplenishmentHistoryWidget,\n};\n\nregistry.category(\"fields\").add(\"replenishment_history_widget\", replenishmentHistoryWidget);\n", "/** @odoo-module */\nimport { registry } from \"@web/core/registry\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { Component } from \"@odoo/owl\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\n/**\n * Extend this to add functionality to Popover (custom methods etc.)\n * need to extend PopoverWidgetField as well and set its Popover Component to new extension\n */\nexport class PopoverComponent extends Component {\n    static template = \"stock.popoverContent\";\n    static props = [\"record\", \"*\"];\n}\n\n/**\n * Widget Popover for JSON field (char), renders a popover above an icon button on click\n * {\n *  'msg': '<CONTENT OF THE POPOVER>' required if not 'popoverTemplate' is given,\n *  'icon': '<FONT AWESOME CLASS>' default='fa-info-circle',\n *  'color': '<COLOR CLASS OF ICON>' default='text-primary',\n *  'position': <POSITION OF THE POPOVER> default='top',\n *  'popoverTemplate': '<TEMPLATE OF THE POPOVER>' default='stock.popoverContent'\n *   pass a template for popover to use, other data passed in JSON field will be passed\n *   to popover template inside props (ex. props.someValue), must be owl template\n * }\n */\n\nexport class PopoverWidgetField extends Component {\n    static template = \"stock.popoverButton\";\n    static components = { Popover: PopoverComponent };\n    static props = {...standardFieldProps};\n    setup(){\n        let fieldValue = this.props.record.data[this.props.name];\n        this.jsonValue = JSON.parse(fieldValue || \"{}\");\n        const position = this.jsonValue.position || \"top\";\n        this.popover = usePopover(this.constructor.components.Popover, { position });\n        this.color = this.jsonValue.color || 'text-primary';\n        this.icon = this.jsonValue.icon || 'fa-info-circle';\n    }\n\n    showPopup(ev){\n        this.popover.open(ev.currentTarget, { ...this.jsonValue, record: this.props.record });\n    }\n}\n\nexport const popoverWidgetField = {\n    component: PopoverWidgetField,\n    supportedTypes: ['char'],\n};\n\nregistry.category(\"fields\").add(\"popover_widget\", popoverWidgetField);\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { Many2OneField, many2OneField } from \"@web/views/fields/many2one/many2one_field\";\n\n\nexport class StockPickFrom extends Many2OneField {\n    get displayName() {\n        return super.displayName || this._quant_display_name();\n    }\n\n    get value() {\n        return super.value || [0, this._quant_display_name()];\n    }\n\n    _quant_display_name() {\n        let name_parts = [];\n        // if location group is activated\n        const data = this.props.record.data;\n        name_parts.push(data.location_id?.[1])\n        if (data.lot_id) {\n            name_parts.push(data.lot_id?.[1] || data.lot_name)\n        }\n        if (data.package_id) {\n            name_parts.push(data.package_id?.[1])\n        }\n        if (data.owner) {\n            name_parts.push(data.owner?.[1])\n        }\n        const result = name_parts.join(\" - \");\n        if (result) return result;\n        return \"\";\n    }\n}\n\nexport const stockPickFrom = {\n    ...many2OneField,\n    component: StockPickFrom,\n    fieldDependencies: [\n        ...(many2OneField.fieldDependencies || []),\n        // dependencies to build the quant display name\n        { name: \"location_id\", type: \"relation\" },\n        { name: \"location_dest_id\", type: \"relation\" },\n        { name: \"package_id\", type: \"relation\" },\n        { name: \"owner_id\", type: \"relation\" },\n    ],\n};\n\nregistry.category(\"fields\").add(\"pick_from\", stockPickFrom);\n", "/** @odoo-module */\nimport { useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\nimport {\n    PopoverComponent,\n    PopoverWidgetField,\n    popoverWidgetField,\n} from \"@stock/widgets/popover_widget\";\n\nexport class StockRescheculingPopoverComponent extends PopoverComponent {\n    setup(){\n        this.action = useService(\"action\");\n    }\n\n    openElement(ev){\n        this.action.doAction({\n            res_model: ev.currentTarget.getAttribute('element-model'),\n            res_id: parseInt(ev.currentTarget.getAttribute('element-id')),\n            views: [[false, \"form\"]],\n            type: \"ir.actions.act_window\",\n            view_mode: \"form\",\n        });\n    }\n}\n\nexport class StockRescheculingPopover extends PopoverWidgetField {\n    static components = {\n        Popover: StockRescheculingPopoverComponent\n    };\n    setup(){\n        super.setup();\n        this.color = this.jsonValue.color || 'text-danger';\n        this.icon = this.jsonValue.icon || 'fa-exclamation-triangle';\n    }\n\n    showPopup(ev){\n        if (!this.jsonValue.late_elements){\n            return;\n        }\n        super.showPopup(ev);\n    }\n}\n\nregistry.category(\"fields\").add(\"stock_rescheduling_popover\", {\n    ...popoverWidgetField,\n    component: StockRescheculingPopover,\n});\n", "/** @odoo-module **/\nimport { BurgerMenu } from \"@web/webclient/burger_menu/burger_menu\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\n\nexport class EnterpriseBurgerMenu extends BurgerMenu {\n    setup() {\n        super.setup();\n        this.hm = useService(\"home_menu\");\n    }\n\n    get currentApp() {\n        return !this.hm.hasHomeMenu && super.currentApp;\n    }\n}\n\nconst systrayItem = {\n    Component: EnterpriseBurgerMenu,\n};\n\nregistry.category(\"systray\").add(\"burger_menu\", systrayItem, { sequence: 0, force: true });\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { cookie as cookieManager } from \"@web/core/browser/cookie\";\n\nexport function switchColorSchemeItem(env) {\n    return {\n        type: \"switch\",\n        id: \"color_scheme.switch_theme\",\n        description: _t(\"Dark Mode\"),\n        callback: () => {\n            const cookie = cookieManager.get(\"color_scheme\");\n            const scheme = cookie === \"dark\" ? \"light\" : \"dark\";\n            env.services.color_scheme.switchToColorScheme(scheme);\n        },\n        isChecked: cookieManager.get(\"color_scheme\") === \"dark\",\n        sequence: 30,\n    };\n}\n", "import { registry } from \"@web/core/registry\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { cookie } from \"@web/core/browser/cookie\";\n\nimport { switchColorSchemeItem } from \"./color_scheme_menu_items\";\n\nconst serviceRegistry = registry.category(\"services\");\nconst userMenuRegistry = registry.category(\"user_menuitems\");\n\nexport const colorSchemeService = {\n    dependencies: [\"ui\"],\n\n    start(env, { ui }) {\n        userMenuRegistry.add(\"color_scheme.switch\", switchColorSchemeItem);\n        return {\n            switchToColorScheme: (scheme) => {\n                cookie.set(\"color_scheme\", scheme);\n                ui.block();\n                this.reload();\n            },\n        };\n    },\n    reload() {\n        browser.location.reload();\n    },\n};\nserviceRegistry.add(\"color_scheme\", colorSchemeService);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { session } from \"@web/session\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { deserializeDateTime, serializeDate, formatDate } from \"@web/core/l10n/dates\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ExpirationPanel } from \"./expiration_panel\";\nimport { cookie } from \"@web/core/browser/cookie\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nconst { DateTime } = luxon;\nimport { Component, xml, useState } from \"@odoo/owl\";\n\nfunction daysUntil(datetime) {\n    const duration = datetime.diff(DateTime.utc(), \"days\");\n    return Math.round(duration.values.days);\n}\n\nexport class SubscriptionManager {\n    constructor(env, { orm, notification }) {\n        this.env = env;\n        this.orm = orm;\n        this.notification = notification;\n        if (session.expiration_date) {\n            this.expirationDate = deserializeDateTime(session.expiration_date);\n        } else {\n            // If no date found, assume 1 month and hope for the best\n            this.expirationDate = DateTime.utc().plus({ days: 30 });\n        }\n        this.expirationReason = session.expiration_reason;\n        // Hack: we need to know if there is at least one app installed (except from App and\n        // Settings). We use mail to do that, as it is a dependency of almost every addon. To\n        // determine whether mail is installed or not, we check for the presence of the key\n        // \"storeData\" in session_info, as it is added in mail.\n        this.hasInstalledApps = \"storeData\" in session;\n        // \"user\" or \"admin\"\n        this.warningType = session.warning;\n        this.lastRequestStatus = null;\n        this.isWarningHidden = cookie.get(\"oe_instance_hide_panel\");\n    }\n\n    get formattedExpirationDate() {\n        return formatDate(this.expirationDate, { format: \"DDD\" });\n    }\n\n    get daysLeft() {\n        return daysUntil(this.expirationDate);\n    }\n\n    get unregistered() {\n        return [\"trial\", \"demo\", false].includes(this.expirationReason);\n    }\n\n    hideWarning() {\n        // Hide warning for 24 hours.\n        cookie.set(\"oe_instance_hide_panel\", true, 24 * 60 * 60);\n        this.isWarningHidden = true;\n    }\n\n    async buy() {\n        const limitDate = serializeDate(DateTime.utc().minus({ days: 15 }));\n        const args = [\n            [\n                [\"share\", \"=\", false],\n                [\"login_date\", \">=\", limitDate],\n            ],\n        ];\n        const nbUsers = await this.orm.call(\"res.users\", \"search_count\", args);\n        browser.location = `https://www.odoo.com/odoo-enterprise/upgrade?num_users=${nbUsers}`;\n    }\n    /**\n     * Save the registration code then triggers a ping to submit it.\n     */\n    async submitCode(enterpriseCode) {\n        const [oldDate, ] = await Promise.all([\n            this.orm.call(\"ir.config_parameter\", \"get_param\", [\"database.expiration_date\"]),\n            this.orm.call(\"ir.config_parameter\", \"set_param\", [\n                \"database.enterprise_code\",\n                enterpriseCode,\n            ])\n        ]);\n\n        await this.orm.call(\"publisher_warranty.contract\", \"update_notification\", [[]]);\n\n        const [linkedSubscriptionUrl, linkedEmail, expirationDate] = await Promise.all([\n            this.orm.call(\"ir.config_parameter\", \"get_param\", [\n                \"database.already_linked_subscription_url\",\n            ]),\n            this.orm.call(\"ir.config_parameter\", \"get_param\", [\"database.already_linked_email\"]),\n            this.orm.call(\"ir.config_parameter\", \"get_param\", [\n                \"database.expiration_date\",\n            ])\n        ]);\n\n        if (linkedSubscriptionUrl) {\n            this.lastRequestStatus = \"link\";\n            this.linkedSubscriptionUrl = linkedSubscriptionUrl;\n            this.mailDeliveryStatus = null;\n            this.linkedEmail = linkedEmail;\n        } else if (expirationDate !== oldDate) {\n            this.lastRequestStatus = \"success\";\n            this.expirationDate = deserializeDateTime(expirationDate);\n            if (this.daysLeft > 30) {\n                this.notification.add(\n                    _t(\n                        \"Thank you, your registration was successful! Your database is valid until %s.\",\n                        this.formattedExpirationDate\n                    ),\n                    { type: \"success\" }\n                );\n            }\n        } else {\n            this.lastRequestStatus = \"error\";\n        }\n    }\n\n    async checkStatus() {\n        await this.orm.call(\"publisher_warranty.contract\", \"update_notification\", [[]]);\n\n        const expirationDateStr = await this.orm.call(\"ir.config_parameter\", \"get_param\", [\n            \"database.expiration_date\",\n        ]);\n        this.lastRequestStatus = \"update\";\n        this.expirationDate = deserializeDateTime(expirationDateStr);\n    }\n\n    async sendUnlinkEmail() {\n        const sendUnlinkInstructionsUrl = await this.orm.call(\"ir.config_parameter\", \"get_param\", [\n            \"database.already_linked_send_mail_url\",\n        ]);\n        this.mailDeliveryStatus = \"ongoing\";\n        const { result, reason } = await rpc(sendUnlinkInstructionsUrl);\n        if (result) {\n            this.mailDeliveryStatus = \"success\";\n        } else {\n            this.mailDeliveryStatus = \"fail\";\n            this.mailDeliveryStatusError = reason;\n        }\n    }\n\n    async renew() {\n        const enterpriseCode = await this.orm.call(\"ir.config_parameter\", \"get_param\", [\n            \"database.enterprise_code\",\n        ]);\n\n        const url = \"https://www.odoo.com/odoo-enterprise/renew\";\n        const contractQueryString = enterpriseCode ? `?contract=${enterpriseCode}` : \"\";\n        browser.location = `${url}${contractQueryString}`;\n    }\n\n    async upsell() {\n        const limitDate = serializeDate(DateTime.utc().minus({ days: 15 }));\n        const [enterpriseCode, nbUsers] = await Promise.all([\n            this.orm.call(\"ir.config_parameter\", \"get_param\", [\"database.enterprise_code\"]),\n            this.orm.call(\"res.users\", \"search_count\", [\n                [\n                    [\"share\", \"=\", false],\n                    [\"login_date\", \">=\", limitDate],\n                ],\n            ]),\n        ]);\n        const url = \"https://www.odoo.com/odoo-enterprise/upsell\";\n        const contractQueryString = enterpriseCode ? `&contract=${enterpriseCode}` : \"\";\n        browser.location = `${url}?num_users=${nbUsers}${contractQueryString}`;\n    }\n}\n\nclass ExpiredSubscriptionBlockUI extends Component {\n    static props = {};\n    // TODO the \"o_blockUI\" div in there seems useless (it has 0 height and thus displays and does nothing)\n    static template = xml`\n        <t t-if=\"subscription.daysLeft &lt;= 0\">\n            <div class=\"o_blockUI\"/>\n            <div style=\"position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: 1100\" class=\"d-flex align-items-center justify-content-center\">\n                <ExpirationPanel/>\n            </div>\n        </t>`;\n    static components = { ExpirationPanel };\n    setup() {\n        this.subscription = useState(useService(\"enterprise_subscription\"));\n    }\n}\n\nexport const enterpriseSubscriptionService = {\n    name: \"enterprise_subscription\",\n    dependencies: [\"orm\", \"notification\"],\n    start(env, { orm, notification }) {\n        registry\n            .category(\"main_components\")\n            .add(\"expired_subscription_block_ui\", { Component: ExpiredSubscriptionBlockUI });\n        return new SubscriptionManager(env, { orm, notification });\n    },\n};\n\nregistry.category(\"services\").add(\"enterprise_subscription\", enterpriseSubscriptionService);\n", "/** @odoo-module **/\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Transition } from \"@web/core/transition\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Component, useState, useRef } from \"@odoo/owl\";\n\n/**\n * Expiration panel\n *\n * Component representing the banner located on top of the home menu. Its purpose\n * is to display the expiration state of the current database and to help the\n * user to buy/renew its subscription.\n * @extends Component\n */\nexport class ExpirationPanel extends Component {\n    static template = \"DatabaseExpirationPanel\";\n    static props = {};\n    static components = { Transition };\n\n    setup() {\n        this.subscription = useState(useService(\"enterprise_subscription\"));\n\n        this.state = useState({\n            displayRegisterForm: false,\n        });\n\n        this.inputRef = useRef(\"input\");\n    }\n\n    get buttonText() {\n        return this.subscription.lastRequestStatus === \"error\" ? _t(\"Retry\") : _t(\"Register\");\n    }\n\n    get alertType() {\n        if (this.subscription.lastRequestStatus === \"success\") {\n            return \"success\";\n        }\n        const { daysLeft } = this.subscription;\n        if (daysLeft <= 6) {\n            return \"danger\";\n        } else if (daysLeft <= 16) {\n            return \"warning\";\n        }\n        return \"info\";\n    }\n\n    get expirationMessage() {\n        const { daysLeft } = this.subscription;\n        if (daysLeft <= 0) {\n            return _t(\"This database has expired. \");\n        }\n        const delay = daysLeft === 30 ? _t(\"1 month\") : _t(\"%s days\", daysLeft);\n        if (this.subscription.expirationReason === \"demo\") {\n            return _t(\"This demo database will expire in %s. \", delay);\n        }\n        return _t(\"This database will expire in %s. \", delay);\n    }\n\n    showRegistrationForm() {\n        this.state.displayRegisterForm = !this.state.displayRegisterForm;\n    }\n\n    async onCodeSubmit() {\n        const enterpriseCode = this.inputRef.el.value;\n        if (!enterpriseCode) {\n            return;\n        }\n        await this.subscription.submitCode(enterpriseCode);\n        if (this.subscription.lastRequestStatus === \"success\") {\n            this.state.displayRegisterForm = false;\n        } else {\n            this.state.buttonText = _t(\"Retry\");\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { hasTouch, isIosApp, isMacOS } from \"@web/core/browser/feature_detection\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { ExpirationPanel } from \"./expiration_panel\";\nimport { useSortable } from \"@web/core/utils/sortable_owl\";\n\nimport {\n    Component,\n    useExternalListener,\n    onMounted,\n    onPatched,\n    onWillUpdateProps,\n    useState,\n    useRef,\n} from \"@odoo/owl\";\n\nclass FooterComponent extends Component {\n    static template = \"web_enterprise.HomeMenu.CommandPalette.Footer\";\n    static props = {\n        //prop added by the command palette\n        switchNamespace: { type: Function, optional: true },\n    };\n\n    setup() {\n        this.controlKey = isMacOS() ? \"COMMAND\" : \"CONTROL\";\n    }\n}\n/**\n * Home menu\n *\n * This component handles the display and navigation between the different\n * available applications and menus.\n * @extends Component\n */\nexport class HomeMenu extends Component {\n    static template = \"web_enterprise.HomeMenu\";\n    static components = { ExpirationPanel };\n    static props = {\n        apps: {\n            type: Array,\n            element: {\n                type: Object,\n                shape: {\n                    actionID: Number,\n                    href: String,\n                    appID: Number,\n                    id: Number,\n                    label: String,\n                    parents: String,\n                    webIcon: {\n                        type: [\n                            Boolean,\n                            String,\n                            {\n                                type: Object,\n                                optional: 1,\n                                shape: {\n                                    iconClass: String,\n                                    color: String,\n                                    backgroundColor: String,\n                                },\n                            },\n                        ],\n                        optional: true,\n                    },\n                    webIconData: { type: String, optional: 1 },\n                    xmlid: String,\n                },\n            },\n        },\n        reorderApps: { type: Function },\n    };\n\n    /**\n     * @param {Object} props\n     * @param {Object[]} props.apps application icons\n     * @param {number} props.apps[].actionID\n     * @param {number} props.apps[].id\n     * @param {string} props.apps[].label\n     * @param {string} props.apps[].parents\n     * @param {(boolean|string|Object)} props.apps[].webIcon either:\n     *      - boolean: false (no webIcon)\n     *      - string: path to Odoo icon file\n     *      - Object: customized icon (background, class and color)\n     * @param {string} [props.apps[].webIconData]\n     * @param {string} props.apps[].xmlid\n     * @param {function} props.reorderApps\n     */\n    setup() {\n        this.command = useService(\"command\");\n        this.menus = useService(\"menu\");\n        this.homeMenuService = useService(\"home_menu\");\n        this.subscription = useState(useService(\"enterprise_subscription\"));\n        this.ui = useService(\"ui\");\n        this.state = useState({\n            focusedIndex: null,\n            isIosApp: isIosApp(),\n        });\n        this.inputRef = useRef(\"input\");\n        this.rootRef = useRef(\"root\");\n        this.pressTimer;\n\n        if (!this.env.isSmall) {\n            this._registerHotkeys();\n        }\n\n        useSortable({\n            enable: this._enableAppsSorting,\n            // Params\n            ref: this.rootRef,\n            elements: \".o_draggable\",\n            cursor: \"move\",\n            delay: 500,\n            tolerance: 10,\n            // Hooks\n            onWillStartDrag: (params) => this._sortStart(params),\n            onDrop: (params) => this._sortAppDrop(params),\n        });\n\n        onWillUpdateProps(() => {\n            // State is reset on each remount\n            this.state.focusedIndex = null;\n        });\n\n        onMounted(() => {\n            if (!hasTouch()) {\n                this._focusInput();\n            }\n        });\n\n        onPatched(() => {\n            if (this.state.focusedIndex !== null && !this.env.isSmall) {\n                const selectedItem = document.querySelector(\".o_home_menu .o_menuitem.o_focused\");\n                // When TAB is managed externally the class o_focused disappears.\n                if (selectedItem) {\n                    // Center window on the focused item\n                    selectedItem.scrollIntoView({ block: \"center\" });\n                }\n            }\n        });\n    }\n\n    //--------------------------------------------------------------------------\n    // Getters\n    //--------------------------------------------------------------------------\n\n    /**\n     * @returns {Object[]}\n     */\n    get displayedApps() {\n        return this.props.apps;\n    }\n\n    /**\n     * @returns {number}\n     */\n    get maxIconNumber() {\n        const w = window.innerWidth;\n        if (w < 576) {\n            return 3;\n        } else if (w < 768) {\n            return 4;\n        } else {\n            return 6;\n        }\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Object} menu\n     * @returns {Promise}\n     */\n    _openMenu(menu) {\n        return this.menus.selectMenu(menu);\n    }\n\n    /**\n     * Update this.state.focusedIndex if not null.\n     * @private\n     * @param {string} cmd\n     */\n    _updateFocusedIndex(cmd) {\n        const nbrApps = this.displayedApps.length;\n        const lastIndex = nbrApps - 1;\n        const focusedIndex = this.state.focusedIndex;\n        if (lastIndex < 0) {\n            return;\n        }\n        if (focusedIndex === null) {\n            this.state.focusedIndex = 0;\n            return;\n        }\n        const lineNumber = Math.ceil(nbrApps / this.maxIconNumber);\n        const currentLine = Math.ceil((focusedIndex + 1) / this.maxIconNumber);\n        let newIndex;\n        switch (cmd) {\n            case \"previousElem\":\n                newIndex = focusedIndex - 1;\n                break;\n            case \"nextElem\":\n                newIndex = focusedIndex + 1;\n                break;\n            case \"previousColumn\":\n                if (focusedIndex % this.maxIconNumber) {\n                    // app is not the first one on its line\n                    newIndex = focusedIndex - 1;\n                } else {\n                    newIndex =\n                        focusedIndex + Math.min(lastIndex - focusedIndex, this.maxIconNumber - 1);\n                }\n                break;\n            case \"nextColumn\":\n                if (focusedIndex === lastIndex || (focusedIndex + 1) % this.maxIconNumber === 0) {\n                    // app is the last one on its line\n                    newIndex = (currentLine - 1) * this.maxIconNumber;\n                } else {\n                    newIndex = focusedIndex + 1;\n                }\n                break;\n            case \"previousLine\":\n                if (currentLine === 1) {\n                    newIndex = focusedIndex + (lineNumber - 1) * this.maxIconNumber;\n                    if (newIndex > lastIndex) {\n                        newIndex = lastIndex;\n                    }\n                } else {\n                    // we go to the previous line on same column\n                    newIndex = focusedIndex - this.maxIconNumber;\n                }\n                break;\n            case \"nextLine\":\n                if (currentLine === lineNumber) {\n                    newIndex = focusedIndex % this.maxIconNumber;\n                } else {\n                    // we go to the next line on the closest column\n                    newIndex =\n                        focusedIndex + Math.min(this.maxIconNumber, lastIndex - focusedIndex);\n                }\n                break;\n        }\n        // if newIndex is out of bounds -> normalize it\n        if (newIndex < 0) {\n            newIndex = lastIndex;\n        } else if (newIndex > lastIndex) {\n            newIndex = 0;\n        }\n        this.state.focusedIndex = newIndex;\n    }\n\n    _focusInput() {\n        if (!this.env.isSmall && this.inputRef.el) {\n            this.inputRef.el.focus({ preventScroll: true });\n        }\n    }\n\n    _enableAppsSorting() {\n        return true;\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @param {Object} params\n     * @param {HTMLElement} params.element\n     * @param {HTMLElement} params.previous\n     */\n    _sortAppDrop({ element, previous }) {\n        const order = this.props.apps.map((app) => app.xmlid);\n        const elementId = element.children[0].dataset.menuXmlid;\n        const elementIndex = order.indexOf(elementId);\n        // first remove dragged element\n        order.splice(elementIndex, 1);\n        if (previous) {\n            const prevIndex = order.indexOf(previous.children[0].dataset.menuXmlid);\n            // insert dragged element after previous element\n            order.splice(prevIndex + 1, 0, elementId);\n        } else {\n            // insert dragged element at beginning if no previous element\n            order.splice(0, 0, elementId);\n        }\n        // apply new order\n        this.props.reorderApps(order);\n        user.setUserSettings(\"homemenu_config\", JSON.stringify(order));\n    }\n\n    /**\n     * @param {Object} params\n     * @param {HTMLElement} params.element\n     */\n    _sortStart({ element, addClass }) {\n        addClass(element.children[0], \"o_dragged_app\");\n    }\n\n    /**\n     * @private\n     * @param {Object} app\n     */\n    _onAppClick(app) {\n        this._openMenu(app);\n    }\n\n    /**\n     * @private\n     */\n    _registerHotkeys() {\n        const hotkeys = [\n            [\"ArrowDown\", () => this._updateFocusedIndex(\"nextLine\")],\n            [\"ArrowRight\", () => this._updateFocusedIndex(\"nextColumn\")],\n            [\"ArrowUp\", () => this._updateFocusedIndex(\"previousLine\")],\n            [\"ArrowLeft\", () => this._updateFocusedIndex(\"previousColumn\")],\n            [\"Tab\", () => this._updateFocusedIndex(\"nextElem\")],\n            [\"shift+Tab\", () => this._updateFocusedIndex(\"previousElem\")],\n            [\n                \"Enter\",\n                () => {\n                    const menu = this.displayedApps[this.state.focusedIndex];\n                    if (menu) {\n                        this._openMenu(menu);\n                    }\n                },\n            ],\n            [\"Escape\", () => this.homeMenuService.toggle(false)],\n        ];\n        hotkeys.forEach((hotkey) => {\n            useHotkey(...hotkey, {\n                allowRepeat: true,\n            });\n        });\n        useExternalListener(window, \"keydown\", this._onKeydownFocusInput);\n    }\n\n    _onKeydownFocusInput() {\n        if (\n            document.activeElement !== this.inputRef.el &&\n            this.ui.activeElement === document &&\n            ![\"TEXTAREA\", \"INPUT\"].includes(document.activeElement.tagName)\n        ) {\n            this._focusInput();\n        }\n    }\n\n    _onInputSearch() {\n        const onClose = () => {\n            this._focusInput();\n            this.inputRef.el.value = \"\";\n        };\n        const searchValue = this.compositionStart ? \"/\" : `/${this.inputRef.el.value.trim()}`;\n        this.compositionStart = false;\n        this.command.openMainPalette({ searchValue, FooterComponent }, onClose);\n    }\n\n    _onInputBlur() {\n        if (hasTouch()) {\n            return;\n        }\n        // if we blur search input to focus on body (eg. click on any\n        // non-interactive element) restore focus to avoid IME input issue\n        setTimeout(() => {\n            if (document.activeElement === document.body && this.ui.activeElement === document) {\n                this._focusInput();\n            }\n        }, 0);\n    }\n\n    _onCompositionStart() {\n        this.compositionStart = true;\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { Mutex } from \"@web/core/utils/concurrency\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { computeAppsAndMenuItems, reorderApps } from \"@web/webclient/menus/menu_helpers\";\nimport {\n    ControllerNotFoundError,\n    standardActionServiceProps,\n} from \"@web/webclient/actions/action_service\";\nimport { HomeMenu } from \"./home_menu\";\n\nimport { Component, onMounted, onWillUnmount, useState, reactive, xml } from \"@odoo/owl\";\n\nexport const homeMenuService = {\n    dependencies: [\"action\"],\n    start(env) {\n        const state = reactive({\n            hasHomeMenu: false, // true iff the HomeMenu is currently displayed\n            hasBackgroundAction: false, // true iff there is an action behind the HomeMenu\n            toggle,\n        });\n        const mutex = new Mutex(); // used to protect against concurrent toggling requests\n        class HomeMenuAction extends Component {\n            static components = { HomeMenu };\n            static target = \"current\";\n            static props = { ...standardActionServiceProps };\n            static template = xml`<HomeMenu t-props=\"homeMenuProps\"/>`;\n            static displayName = _t(\"Home\");\n\n            setup() {\n                this.menus = useService(\"menu\");\n                const homemenuConfig = JSON.parse(user.settings?.homemenu_config || \"null\");\n                const apps = useState(\n                    computeAppsAndMenuItems(this.menus.getMenuAsTree(\"root\")).apps\n                );\n                if (homemenuConfig) {\n                    reorderApps(apps, homemenuConfig);\n                }\n                this.homeMenuProps = {\n                    apps: apps,\n                    reorderApps: (order) => {\n                        reorderApps(apps, order);\n                    },\n                };\n                onMounted(() => this.onMounted());\n                onWillUnmount(this.onWillUnmount);\n            }\n            async onMounted() {\n                const { breadcrumbs } = this.env.config;\n                state.hasHomeMenu = true;\n                state.hasBackgroundAction = breadcrumbs.length > 0;\n                this.env.bus.trigger(\"HOME-MENU:TOGGLED\");\n            }\n            onWillUnmount() {\n                state.hasHomeMenu = false;\n                state.hasBackgroundAction = false;\n                this.env.bus.trigger(\"HOME-MENU:TOGGLED\");\n            }\n        }\n\n        registry.category(\"actions\").add(\"menu\", HomeMenuAction);\n\n        env.bus.addEventListener(\"HOME-MENU:TOGGLED\", () => {\n            document.body.classList.toggle(\"o_home_menu_background\", state.hasHomeMenu);\n        });\n\n        async function toggle(show) {\n            return mutex.exec(async () => {\n                show = show === undefined ? !state.hasHomeMenu : Boolean(show);\n                if (show !== state.hasHomeMenu) {\n                    if (show) {\n                        await env.services.action.doAction(\"menu\");\n                    } else {\n                        try {\n                            await env.services.action.restore();\n                        } catch (err) {\n                            if (!(err instanceof ControllerNotFoundError)) {\n                                throw err;\n                            }\n                        }\n                    }\n                }\n                // hack: wait for a tick to ensure that the url has been updated before\n                // switching again\n                return new Promise((r) => setTimeout(r));\n            });\n        }\n\n        return state;\n    },\n};\n\nregistry.category(\"services\").add(\"home_menu\", homeMenuService);\n", "/** @odoo-module **/\n\nimport { NavBar } from \"@web/webclient/navbar/navbar\";\nimport { useService, useBus } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useState, useEffect, useRef } from \"@odoo/owl\";\n\nexport class EnterpriseNavBar extends NavBar {\n    static template = \"web_enterprise.EnterpriseNavBar\";\n    setup() {\n        super.setup();\n        this.hm = useState(useService(\"home_menu\"));\n        this.pwa = useService(\"pwa\");\n        this.menuAppsRef = useRef(\"menuApps\");\n        this.navRef = useRef(\"nav\");\n        this._busToggledCallback = () => this._updateMenuAppsIcon();\n        useBus(this.env.bus, \"HOME-MENU:TOGGLED\", this._busToggledCallback);\n        useEffect(() => this._updateMenuAppsIcon());\n    }\n    get hasBackgroundAction() {\n        return this.hm.hasBackgroundAction;\n    }\n    get isInApp() {\n        return !this.hm.hasHomeMenu;\n    }\n\n    _openAppMenuSidebar() {\n        if (this.hm.hasHomeMenu) {\n            this.hm.toggle(false);\n        } else {\n            this.state.isAppMenuSidebarOpened = true;\n        }\n    }\n    _updateMenuAppsIcon() {\n        const menuAppsEl = this.menuAppsRef.el;\n        menuAppsEl.classList.toggle(\"o_hidden\", !this.isInApp && !this.hasBackgroundAction);\n        menuAppsEl.classList.toggle(\n            \"o_menu_toggle_back\",\n            !this.isInApp && this.hasBackgroundAction\n        );\n        if (!this.isScopedApp) {\n            const title =\n                !this.isInApp && this.hasBackgroundAction ? _t(\"Previous view\") : _t(\"Home menu\");\n            menuAppsEl.title = title;\n            menuAppsEl.ariaLabel = title;\n        }\n\n        const menuBrand = this.navRef.el.querySelector(\".o_menu_brand\");\n        if (menuBrand) {\n            menuBrand.classList.toggle(\"o_hidden\", !this.isInApp);\n        }\n\n        const menuBrandIcon = this.navRef.el.querySelector(\".o_menu_brand_icon\");\n        if (menuBrandIcon) {\n            menuBrandIcon.classList.toggle(\"o_hidden\", !this.isInApp);\n        }\n\n        const appSubMenus = this.appSubMenus.el;\n        if (appSubMenus) {\n            appSubMenus.classList.toggle(\"o_hidden\", !this.isInApp);\n        }\n\n        const breadcrumb = this.navRef.el.querySelector(\".o_breadcrumb\");\n        if (breadcrumb) {\n            breadcrumb.classList.toggle(\"o_hidden\", !this.isInApp);\n        }\n    }\n\n    /**\n     * @override\n     */\n    onAllAppsBtnClick() {\n        super.onAllAppsBtnClick();\n        this.hm.toggle(true);\n        this._closeAppMenuSidebar();\n    }\n}\n", "/** @odoo-module */\nimport { browser } from \"@web/core/browser/browser\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { useChildRef, useService } from \"@web/core/utils/hooks\";\n\nimport { Component, useExternalListener } from \"@odoo/owl\";\n\nexport class PromoteStudioDialog extends Component {\n    static template = \"web_enterprise.PromoteStudioDialog\";\n    static components = { Dialog };\n    static props = {\n        title: String,\n        close: Function,\n    };\n\n    setup() {\n        this.ormService = useService(\"orm\");\n        this.uiService = useService(\"ui\");\n\n        this.modalRef = useChildRef();\n\n        useExternalListener(window, \"mousedown\", this.onWindowMouseDown);\n    }\n\n    async onClickInstallStudio() {\n        this.disableClick = true;\n        this.uiService.block();\n        const modules = await this.ormService.searchRead(\n            \"ir.module.module\",\n            [[\"name\", \"=\", \"web_studio\"]],\n            [\"id\"]\n        );\n        await this.ormService.call(\"ir.module.module\", \"button_immediate_install\", [\n            [modules[0].id],\n        ]);\n        // on rpc call return, the framework unblocks the page\n        // make sure to keep the page blocked until the reload ends.\n        this.uiService.unblock();\n        browser.localStorage.setItem(\"openStudioOnReload\", \"main\");\n        browser.location.reload();\n    }\n\n    /**\n     * Close the dialog on outside click.\n     */\n    onWindowMouseDown(ev) {\n        const dialogContent = this.modalRef.el.querySelector(\".modal-content\");\n        if (!this.disableClick && !dialogContent.contains(ev.target)) {\n            this.props.close();\n        }\n    }\n}\n\nexport class PromoteStudioAutomationDialog extends PromoteStudioDialog {\n    static template = \"web_enterprise.PromoteStudioAutomationDialog\";\n}\n", "/** @odoo-module **/\n\nimport { isDisplayStandalone } from \"@web/core/browser/feature_detection\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { BurgerMenu } from \"@web/webclient/burger_menu/burger_menu\";\nimport { shareUrl } from \"./share_url\";\n\nif (navigator.share && isDisplayStandalone()) {\n    patch(BurgerMenu.prototype, {\n        shareUrl,\n    });\n\n    patch(BurgerMenu, {\n        template: \"web_enterprise.BurgerMenu\",\n    });\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { markup } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { isDisplayStandalone } from \"@web/core/browser/feature_detection\";\nimport { escape } from \"@web/core/utils/strings\";\n\nexport async function shareUrl() {\n    await navigator\n        .share({\n            url: browser.location.href,\n            title: document.title,\n        })\n        .catch((e) => {\n            if (!(e instanceof DOMException && e.name === \"AbortError\")) {\n                throw e;\n            }\n        });\n}\n\nexport function shareUrlMenuItem(env) {\n    const translatedText = _t(\"Share\");\n    return {\n        type: \"item\",\n        hide: env.isSmall || !isDisplayStandalone(),\n        id: \"share_url\",\n        description: markup(\n            `<div class=\"d-flex align-items-center justify-content-between\">\n                <span>${escape(translatedText)}</span>\n                <span class=\"fa fa-share-alt\"></span>\n            </div>`\n        ),\n        callback: shareUrl,\n        sequence: 25,\n    };\n}\n\nif (navigator.share) {\n    registry.category(\"user_menuitems\").add(\"share_url\", shareUrlMenuItem);\n}\n", "/** @odoo-module **/\n\nimport { WebClient } from \"@web/webclient/webclient\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { EnterpriseNavBar } from \"./navbar/navbar\";\n\nexport class WebClientEnterprise extends WebClient {\n    static components = {\n        ...WebClient.components,\n        NavBar: EnterpriseNavBar,\n    };\n    setup() {\n        super.setup();\n        this.hm = useService(\"home_menu\");\n    }\n    _loadDefaultApp() {\n        return this.hm.toggle(true);\n    }\n}\n", "/* @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { KanbanHeader } from \"@web/views/kanban/kanban_header\";\nimport { PromoteStudioAutomationDialog } from \"@web_enterprise/webclient/promote_studio_dialog/promote_studio_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\n\npatch(KanbanHeader.prototype, {\n    /**\n     * @override\n     */\n    get permissions() {\n        const permissions = super.permissions;\n        Object.defineProperty(permissions, \"canEditAutomations\", {\n            get: () => user.isAdmin,\n            configurable: true,\n        });\n        return permissions;\n    },\n\n    async openAutomations() {\n        if (typeof this._openAutomations === \"function\") {\n            // this is the case if base_automation is installed\n            return this._openAutomations();\n        } else {\n            this.env.services.dialog.add(PromoteStudioAutomationDialog, {\n                title: _t(\"Odoo Studio - Customize workflows in minutes\"),\n            });\n        }\n    },\n});\n\nregistry.category(\"kanban_header_config_items\").add(\n    \"open_automations\",\n    {\n        label: _t(\"Automations\"),\n        method: \"openAutomations\",\n        isVisible: ({ permissions }) => permissions.canEditAutomations,\n        class: \"o_column_automations\",\n    },\n    { sequence: 25, force: true }\n);\n", "/** @odoo-module */\n\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { PromoteStudioDialog } from \"@web_enterprise/webclient/promote_studio_dialog/promote_studio_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { onWillDestroy, useState } from \"@odoo/owl\";\n\nexport const patchListRendererDesktop = () => ({\n    setup() {\n        super.setup(...arguments);\n        this.actionService = useService(\"action\");\n        const list = this.props.list;\n\n        const { actionId, actionType } = this.env.config || {};\n\n        // Start by determining if the current ListRenderer is in a context that would\n        // allow the edition of the arch by studio.\n        // It needs to be a full list view, in an action\n        // (not a X2Many list, and not an \"embedded\" list in another component)\n        // Also, there is not enough information when an action is in target new,\n        // and this use case is fairly outside of the feature's scope\n        const isPotentiallyEditable =\n            !isMobileOS() &&\n            !this.env.inDialog &&\n            user.isSystem &&\n            list === list.model.root &&\n            actionId &&\n            actionType === \"ir.actions.act_window\";\n        this.studioEditable = useState({ value: isPotentiallyEditable });\n\n        if (isPotentiallyEditable) {\n            const computeStudioEditable = (action) => {\n                // Finalize the computation when the actionService is ready.\n                // The following code is copied from studioService.\n                if (!action.xml_id) {\n                    return false;\n                }\n                if (\n                    action.res_model.indexOf(\"settings\") > -1 &&\n                    action.res_model.indexOf(\"x_\") !== 0\n                ) {\n                    return false; // settings views aren't editable; but x_settings is\n                }\n                if (action.res_model === \"board.board\") {\n                    return false; // dashboard isn't editable\n                }\n                if (action.view_mode === \"qweb\") {\n                    // Apparently there is a QWebView that allows to\n                    // implement ActWindow actions that are completely custom\n                    // but not editable by studio\n                    return false;\n                }\n                if (action.res_model === \"knowledge.article\") {\n                    // The knowledge form view is very specific and custom, it doesn't make sense\n                    // to edit it. Editing the list and kanban is more debatable, but for simplicity's sake\n                    // we set them to not editable too.\n                    return false;\n                }\n                return Boolean(action.res_model);\n            };\n            const onUiUpdated = () => {\n                const action = this.actionService.currentController.action;\n                if (action.id === actionId) {\n                    this.studioEditable.value = computeStudioEditable(action);\n                }\n                stopListening();\n            };\n            const stopListening = () =>\n                this.env.bus.removeEventListener(\"ACTION_MANAGER:UI-UPDATED\", onUiUpdated);\n            this.env.bus.addEventListener(\"ACTION_MANAGER:UI-UPDATED\", onUiUpdated);\n\n            onWillDestroy(stopListening);\n        }\n    },\n\n    isStudioEditable() {\n        return this.studioEditable.value;\n    },\n\n    get displayOptionalFields() {\n        return this.isStudioEditable() || super.displayOptionalFields;\n    },\n\n    /**\n     * This function opens promote studio dialog\n     *\n     * @private\n     */\n    onSelectedAddCustomField() {\n        this.env.services.dialog.add(PromoteStudioDialog, {\n            title: _t(\"Odoo Studio - Add new fields to any view\"),\n        });\n    },\n});\n\nexport const unpatchListRendererDesktop = patch(ListRenderer.prototype, patchListRendererDesktop());\n", "/** @odoo-module **/\n\nimport ServiceCore from \"@web_mobile/js/services/core\";\n\nimport { onMounted, onPatched, onWillUnmount, useComponent } from \"@odoo/owl\";\n\n/**\n * This hook provides support for executing code when the back button is pressed\n * on the mobile application of Odoo. This actually replaces the default back\n * button behavior so this feature should only be enabled when it is actually\n * useful.\n *\n * The feature is either enabled on mount or, using the `shouldEnable` function\n * argument as condition, when the component is patched. In both cases,\n * the feature is automatically disabled on unmount.\n *\n * @param {function} func the function to execute when the back button is\n *  pressed. The function is called with the custom event as param.\n * @param {function} [shouldEnable] the function to execute when the DOM is\n *  patched to check if the backbutton should be enabled or disabled ;\n *  if undefined will be enabled on mount and disabled on unmount.\n */\nexport function useBackButton(func, shouldEnable) {\n    const component = useComponent();\n    let isEnabled = false;\n\n    /**\n     * Enables the func listener, overriding default back button behavior.\n     */\n    function enable() {\n        ServiceCore.backButtonManager.addListener(component, func);\n        isEnabled = true;\n    }\n\n    /**\n     * Disables the func listener, restoring the default back button behavior if\n     * no other listeners are present.\n     */\n    function disable() {\n        ServiceCore.backButtonManager.removeListener(component);\n        isEnabled = false;\n    }\n\n    onMounted(() => {\n        if (shouldEnable && !shouldEnable()) {\n            return;\n        }\n        enable();\n    });\n\n    onPatched(() => {\n        if (!shouldEnable) {\n            return;\n        }\n        const shouldBeEnabled = shouldEnable();\n        if (shouldBeEnabled && !isEnabled) {\n            enable();\n        } else if (!shouldBeEnabled && isEnabled) {\n            disable();\n        }\n    });\n\n    onWillUnmount(() => {\n        if (isEnabled) {\n            disable();\n        }\n    });\n}\n", "/** @odoo-module **/\n\nimport { user } from \"@web/core/user\";\nimport mobile from \"@web_mobile/js/services/core\";\nimport { isIosApp } from \"@web/core/browser/feature_detection\";\nimport { url } from \"@web/core/utils/urls\";\n\nconst DEFAULT_AVATAR_SIZE = 128;\n\nexport const accountMethodsForMobile = {\n    url,\n    /**\n     * Update the user's account details on the mobile app\n     *\n     * @returns {Promise}\n     */\n    async updateAccount() {\n        if (!mobile.methods.updateAccount) {\n            return;\n        }\n        const base64Avatar = await accountMethodsForMobile.fetchAvatar();\n        return mobile.methods.updateAccount({\n            avatar: base64Avatar.substring(base64Avatar.indexOf(',') + 1),\n            name: user.name,\n            username: user.login,\n        });\n    },\n    /**\n     * Fetch current user's avatar as PNG image\n     *\n     * @returns {Promise} resolved with the dataURL, or rejected if the file is\n     *  empty or if an error occurs.\n     */\n    fetchAvatar() {\n        const avatarUrl = accountMethodsForMobile.url('/web/image', {\n            model: 'res.users',\n            field: 'image_medium',\n            id: user.userId,\n        });\n        return new Promise((resolve, reject) => {\n            const canvas = document.createElement('canvas');\n            canvas.width = DEFAULT_AVATAR_SIZE;\n            canvas.height = DEFAULT_AVATAR_SIZE;\n            const context = canvas.getContext('2d');\n            const image = new Image();\n            image.addEventListener('load', () => {\n                context.drawImage(image, 0, 0, DEFAULT_AVATAR_SIZE, DEFAULT_AVATAR_SIZE);\n                resolve(canvas.toDataURL('image/png'));\n            });\n            image.addEventListener('error', reject);\n            image.src = avatarUrl;\n        });\n    },\n};\n\n/**\n * Mixin to hook into the controller record's saving method and\n * trigger the update of the user's account details on the mobile app.\n *\n * @mixin\n * @name UpdateDeviceAccountControllerMixin\n *\n */\nconst UpdateDeviceAccountControllerMixin = {\n    /**\n     * @override\n     */\n    async save() {\n        const isSaved = await this._super(...arguments);\n        if (!isSaved) {\n            return false;\n        }\n        const updated = accountMethodsForMobile.updateAccount();\n        // Crapy workaround for unupdatable Odoo Mobile App iOS (Thanks Apple :@)\n        if (!isIosApp()){\n            await updated;\n        }\n        return true;\n    },\n};\n\nexport async function updateAccountOnMobileDevice() {\n    const updated = accountMethodsForMobile.updateAccount();\n    // Crapy workaround for unupdatable Odoo Mobile App iOS (Thanks Apple :@)\n    if (!isIosApp()){\n        await updated;\n    }\n}\n\n/**\n * Trigger the update of the user's account details on the mobile app.\n */\naccountMethodsForMobile.updateAccount();\n\nexport default {\n    UpdateDeviceAccountControllerMixin,\n    updateAccountOnMobileDevice,\n};\n", "/** @odoo-module **/\n\nimport mobile from \"@web_mobile/js/services/core\";\nimport { download } from \"@web/core/network/download\";\n\nconst _download = download._download;\n\ndownload._download = async function (options) {\n    if (mobile.methods.downloadFile) {\n        if (odoo.csrf_token) {\n            options.csrf_token = odoo.csrf_token;\n        }\n        mobile.methods.downloadFile(options);\n        // There is no need to wait downloadFile because we delegate this to\n        // Download Manager Service where error handling will be handled correclty.\n        // On our side, we do not want to block the UI and consider the request\n        // as success.\n        return Promise.resolve();\n    } else {\n        return _download.apply(this, arguments);\n    }\n};\n", "/** @odoo-module **/\n\nimport { Popover } from \"@web/core/popover/popover\";\nimport { useBackButton } from \"@web_mobile/js/core/hooks\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Popover.prototype, {\n    setup() {\n        super.setup(...arguments);\n        useBackButton(this.onBackButton.bind(this), () => this.props.target.isConnected);\n    },\n\n    //---------------------------------------------------------------------\n    // Handlers\n    //---------------------------------------------------------------------\n\n    /**\n     * Close popover on back-button\n     * @private\n     */\n    onBackButton() {\n        this.props.close();\n    },\n});\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport mobile from \"@web_mobile/js/services/core\";\n\nfunction mobileErrorHandler(env, error, originalError) {\n    if (mobile.methods.crashManager) {\n        error.originalError = originalError;\n        mobile.methods.crashManager(error);\n    }\n}\nregistry\n    .category(\"error_handlers\")\n    .add(\"web_mobile.errorHandler\", mobileErrorHandler, { sequence: 3 });\n", "import { EventBus } from \"@odoo/owl\";\n\nexport class HookEventBus extends EventBus {\n    /**\n        @param {{}} hooks\n        @param {Function} [hooks.onAddListener]\n        @param {Function} [hooks.onRemoveListener]\n     */\n    constructor(hooks = {}) {\n        super();\n        this.hooks = hooks;\n    }\n\n    addEventListener(eventName, listener) {\n        super.addEventListener(eventName, listener);\n        this.hooks.onAddListener?.(eventName, listener);\n    }\n\n    removeEventListener(eventName, listener) {\n        super.removeEventListener(eventName, listener);\n        this.hooks.onRemoveListener?.(eventName, listener);\n    }\n}\n", "/** @odoo-module **/\n\nimport { HookEventBus } from \"@web_mobile/js/hook_event_bus\";\nimport { registry } from \"@web/core/registry\";\nimport { session } from \"@web/session\";\n\nimport mobile from \"@web_mobile/js/services/core\";\nimport { shortcutItem, switchAccountItem } from \"./user_menu_items\";\n\nconst serviceRegistry = registry.category(\"services\");\nconst userMenuRegistry = registry.category(\"user_menuitems\");\n\nexport const mobileService = {\n    timeBetweenReadsInMs: session.time_between_reads_in_ms || 100,\n    idInterval: null,\n    start() {\n        let listenerCount = 0;\n        this.bus = new HookEventBus({\n            onAddListener: (eventName, listener) => {\n                listenerCount++;\n                if (listenerCount === 1) {\n                    api.enableReader();\n                }\n            },\n            onRemoveListener: (eventName, listener) => {\n                listenerCount--;\n                if (listenerCount === 0) {\n                    api.stopReader();\n                }\n            },\n        });\n\n        const api =  {\n            bus: this.bus,\n            enableReader: this.enableReader,\n            stopReader: this.stopReader,\n        };\n\n        if (mobile.methods.addHomeShortcut) {\n            userMenuRegistry.add(\"web_mobile.shortcut\", shortcutItem);\n        }\n\n        if (mobile.methods.switchAccount) {\n            // remove \"Log Out\" and \"My Odoo.com Account\"\n            userMenuRegistry.remove('log_out');\n            userMenuRegistry.remove('odoo_account');\n\n            userMenuRegistry.add(\"web_mobile.switch\", switchAccountItem);\n        }\n\n        return api;\n    },\n\n    enableReader() {\n        if (mobile.methods.enableReader && mobile.methods.getReaderData) {\n            mobile.methods.enableReader().catch(e => console.error(e));\n            if (!this.idInterval) {\n                this.idInterval = setInterval(async () => {\n                    try {\n                        const value = await mobile.methods.getReaderData();\n                        if (value.success) {\n                            const data = value.data;\n                            if (data.length > 0) {\n                                this.bus.trigger(\"mobile_reader_scanned\", {data});\n                            }\n                        }\n                    } catch (e) {\n                        console.error(e);\n                    }\n                }, this.timeBetweenReadsInMs);\n            }\n        }\n    },\n\n    stopReader() {\n        if (this.idInterval) {\n            clearInterval(this.idInterval);\n            this.idInterval = null;\n        }\n    },\n};\nserviceRegistry.add(\"mobile\", mobileService);\n", "/** @odoo-module **/\n/* global OdooDeviceUtility */\n\nimport { uniqueId } from \"@web/core/utils/functions\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { parseSearchQuery } from \"@web/core/browser/router\";\n\nvar available = typeof OdooDeviceUtility !== 'undefined';\nvar DeviceUtility;\nvar deferreds = {};\nexport var methods = {};\n\nif (available){\n    DeviceUtility = OdooDeviceUtility;\n    delete window.OdooDeviceUtility;\n}\n\n/**\n * Responsible for invoking native methods which called from JavaScript\n *\n * @param {String} name name of action want to perform in mobile\n * @param {Object} args extra arguments for mobile\n *\n * @returns Promise Object\n */\nfunction native_invoke(name, args) {\n    if (args === undefined) {\n        args = {};\n    }\n    var id = uniqueId();\n    args = JSON.stringify(args);\n    DeviceUtility.execute(name, args, id);\n    return new Promise(function (resolve, reject) {\n        deferreds[id] = {\n            successCallback: resolve,\n            errorCallback: reject\n        };\n    });\n}\n\n/**\n * Manages deferred callback from initiate from native mobile\n *\n * @param {String} id callback id\n * @param {Object} result\n */\nwindow.odoo.native_notify = function (id, result) {\n    if (deferreds.hasOwnProperty(id)) {\n        if (result.success) {\n            deferreds[id].successCallback(result);\n        } else {\n            deferreds[id].errorCallback(result);\n        }\n    }\n};\n\nvar plugins = available ? JSON.parse(DeviceUtility.list_plugins()) : [];\nplugins.forEach((plugin) => {\n    methods[plugin.name] = function (args) {\n        return native_invoke(plugin.action, args);\n    };\n});\n\n/**\n * Use to notify an uri hash change on native devices (ios / android)\n */\nif (methods.hashChange) {\n    var currentSearch;\n    browser.addEventListener(\"popstate\", function () {\n        const search = parseSearchQuery(browser.location.search);\n        if (JSON.stringify(currentSearch) !== JSON.stringify(search)) {\n            methods.hashChange(search);\n        }\n        currentSearch = search;\n    });\n}\n\n/**\n * Error related to the registration of a listener to the backbutton event\n */\nclass BackButtonListenerError extends Error {}\n\n/**\n * By using the back button feature the default back button behavior from the\n * app is actually overridden so it is important to keep count to restore the\n * default when no custom listener are remaining.\n */\nclass BackButtonManager {\n\n    constructor() {\n        this._listeners = new Map();\n        this._onGlobalBackButton = this._onGlobalBackButton.bind(this);\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * Enables the func listener, overriding default back button behavior.\n     *\n     * @param {Component} listener\n     * @param {function} func\n     * @throws {BackButtonListenerError} if the listener has already been registered\n     */\n    addListener(listener, func) {\n        if (!methods.overrideBackButton) {\n            return;\n        }\n        if (this._listeners.has(listener)) {\n            throw new BackButtonListenerError(\"This listener was already registered.\");\n        }\n        this._listeners.set(listener, func);\n        if (this._listeners.size === 1) {\n            document.addEventListener('backbutton', this._onGlobalBackButton);\n            methods.overrideBackButton({ enabled: true });\n        }\n    }\n    /**\n     * Disables the func listener, restoring the default back button behavior if\n     * no other listeners are present.\n     *\n     * @param {Component} listener\n     * @throws {BackButtonListenerError} if the listener has already been unregistered\n     */\n    removeListener(listener) {\n        if (!methods.overrideBackButton) {\n            return;\n        }\n        if (!this._listeners.has(listener)) {\n            throw new BackButtonListenerError(\"This listener has already been unregistered.\");\n        }\n        this._listeners.delete(listener);\n        if (this._listeners.size === 0) {\n            document.removeEventListener('backbutton', this._onGlobalBackButton);\n            methods.overrideBackButton({ enabled: false });\n        }\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    _onGlobalBackButton() {\n        const [listener, func] = [...this._listeners].pop();\n        if (listener) {\n            func.apply(listener, arguments);\n        }\n    }\n}\n\nconst backButtonManager = new BackButtonManager();\n\nexport default {\n    BackButtonManager,\n    BackButtonListenerError,\n    backButtonManager,\n    methods,\n};\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport mobile from \"@web_mobile/js/services/core\";\n\nexport function shortcutItem(env) {\n    return {\n        type: \"item\",\n        id: \"web_mobile.shortcut\",\n        description: _t(\"Add to Home Screen\"),\n        callback: () => {\n            const menu = env.services.menu.getCurrentApp();\n            if (menu) {\n                const base64Icon = menu.webIconData;\n                mobile.methods.addHomeShortcut({\n                    title: document.title,\n                    shortcut_url: document.URL,\n                    web_icon: base64Icon.substring(base64Icon.indexOf(\",\") + 1),\n                });\n            } else {\n                env.services.notification.add(_t(\"No shortcut for Home Menu\"));\n            }\n        },\n        sequence: 100,\n    };\n}\n\nexport function switchAccountItem(env) {\n    return {\n        type: \"item\",\n        id: \"web_mobile.switch\",\n        description: _t(\"Switch/Add Account\"),\n        callback: () => {\n            mobile.methods.switchAccount();\n        },\n        sequence: 100,\n    };\n}\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { formView } from \"@web/views/form/form_view\";\nimport { updateAccountOnMobileDevice } from \"@web_mobile/js/core/mixins\";\n\nclass ResUsersPreferenceController extends formView.Controller {\n    onRecordSaved(record) {\n        return updateAccountOnMobileDevice();\n    }\n}\n\nregistry.category(\"views\").add(\"res_users_preferences_form\", {\n    ...formView,\n    Controller: ResUsersPreferenceController,\n});\n", "import { patch } from \"@web/core/utils/patch\";\nimport { Message } from \"@mail/core/common/message\";\n\npatch(Message, {\n    SHADOW_LINK_COLOR: \"#017e84\",\n    SHADOW_LINK_HOVER_COLOR: \"#016b70\",\n});\n", "import { FileViewer } from \"@web/core/file_viewer/file_viewer\";\nimport { patch } from \"@web/core/utils/patch\";\n\nimport { useBackButton } from \"@web_mobile/js/core/hooks\";\n\npatch(FileViewer.prototype, {\n    setup() {\n        super.setup();\n        useBackButton(() => this.close());\n    },\n});\n", "import { ChatWindow } from \"@mail/core/common/chat_window\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { patch } from \"@web/core/utils/patch\";\n\nimport { useBackButton } from \"@web_mobile/js/core/hooks\";\n\npatch(ChatWindow.prototype, {\n    setup() {\n        super.setup();\n        useBackButton(() => this.props.chatWindow.close());\n        this.homeMenuService = useService(\"home_menu\");\n    },\n});\n", "import { MessagingMenu } from \"@mail/core/public_web/messaging_menu\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\nimport { useBackButton } from \"@web_mobile/js/core/hooks\";\n\npatch(MessagingMenu.prototype, {\n    setup() {\n        super.setup();\n        useBackButton(\n            () => this.dropdown.close(),\n            () => this.dropdown.isOpen\n        );\n    },\n});\n", "import { threadActionsRegistry } from \"@mail/core/common/thread_actions\";\n\nthreadActionsRegistry.get(\"expand-discuss\").shouldClearBreadcrumbs = (component) => {\n    return component.homeMenuService.hasHomeMenu;\n};\n", "/** @odoo-module **/\n\n    import { _t } from \"@web/core/l10n/translation\";\n    import { registry } from \"@web/core/registry\";\n    import { patch } from \"@web/core/utils/patch\";\n    import { markup } from \"@odoo/owl\";\n    import { accountTourSteps } from \"@account/js/tours/account\";\n\n    patch(accountTourSteps, {\n        newInvoice() {\n            return [\n                {\n                    trigger: \"button[name=action_create_new]\",\n                    content: _t(\"Now, we'll create your first invoice (accountant)\"),\n                    run: \"click\",\n                }\n            ]\n        },\n    });\n\n\n    registry.category(\"web_tour.tours\").add('account_accountant_tour', {\n            url: \"/odoo\",\n            steps: () => [\n            ...accountTourSteps.goToAccountMenu('Let\u2019s automate your bills, bank transactions and accounting processes.'),\n            // The tour will stop here if there is at least 1 vendor bill in the database.\n            // While not ideal, it is ok, since that means the user obviously knows how to create a vendor bill...\n            {\n                trigger: 'a[name=\"action_create_vendor_bill\"]',\n                content: markup(_t('Create your first vendor bill.<br/><br/><i>Tip: If you don\u2019t have one on hand, use our sample bill.</i>')),\n                tooltipPosition: 'bottom',\n                run: \"click\",\n            }, {\n                trigger: 'button.btn-primary[name=\"action_post\"]',\n                content: _t('After the data extraction, check and validate the bill. If no vendor has been found, add one before validating.'),\n                tooltipPosition: 'bottom',\n                run: \"click\",\n            }, {\n                trigger: '.dropdown-item[data-menu-xmlid=\"account.menu_board_journal_1\"]',\n                content: _t('Let\u2019s go back to the dashboard.'),\n                tooltipPosition: 'bottom',\n                run: \"click\",\n            }, {\n                trigger: 'a[name=\"open_action\"] span:contains(bank)',\n                content: _t('Connect your bank and get your latest transactions.'),\n                tooltipPosition: 'bottom',\n                run: \"click\",\n            }, {\n                trigger: 'button.o-kanban-button-new',\n                content: _t('Create a new transaction.'),\n                run: \"click\",\n            }, {\n                trigger: \"div[name=amount] div input[id=amount_0]\",\n                content: _t(\"Set an amount.\"),\n                tooltipPosition: \"bottom\",\n                run: \"edit -19250.00\",\n            }, {\n                trigger: \"div[name=payment_ref] input[id=payment_ref_0]\",\n                content: _t(\"Set the payment reference.\"),\n                tooltipPosition: \"bottom\",\n                run: \"edit Payment Deco Adict\",\n            }, {\n                trigger: \"button.o_kanban_edit\",\n                content: _t(\"Confirm the transaction.\"),\n                tooltipPosition: \"bottom\",\n                run: \"click\",\n            }, {\n                trigger: '.o_kanban_renderer:not(:has(.o_bank_rec_quick_create)) .o_bank_rec_st_line:not(.o_bank_rec_selected_st_line)',\n                content: _t('Click on a fetched bank transaction to start the reconciliation process.'),\n                run: \"click\",\n            }, {\n                isActive: ['auto'],\n                trigger: '.dropdown-item[data-menu-xmlid=\"account.menu_board_journal_1\"]',\n                content: _t('Let\u2019s go back to the dashboard.'),\n                run: \"click\",\n            },\n        ]\n    });\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { EmbeddedListView } from \"./embedded_list_view\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { useState, onWillUnmount } from \"@odoo/owl\";\n\nexport class BankRecAmlsRenderer extends ListRenderer {\n    setup() {\n        super.setup();\n        this.globalState = useState(this.env.methods.getState());\n\n        onWillUnmount(this.saveSearchState);\n    }\n\n    /** @override **/\n    getRowClass(record) {\n        const classes = super.getRowClass(record);\n        const amlId = this.globalState.bankRecRecordData.selected_aml_ids.currentIds.find((x) => x === record.resId);\n        if (amlId){\n            return `${classes} o_rec_widget_list_selected_item table-info`;\n        }\n        return classes;\n    }\n\n    /** @override **/\n    async onCellClicked(record, column, ev) {\n        const amlId = this.globalState.bankRecRecordData.selected_aml_ids.currentIds.find((x) => x === record.resId);\n        if (amlId) {\n            this.env.config.actionRemoveNewAml(record.resId);\n        } else {\n            this.env.config.actionAddNewAml(record.resId);\n        }\n    }\n\n    /** Backup the search facets in order to restore them when the user comes back on this view. **/\n    saveSearchState() {\n        const initParams = this.globalState.bankRecEmbeddedViewsData.amls;\n        const searchModel = this.env.searchModel;\n        initParams.exportState = {searchModel: JSON.stringify(searchModel.exportState())};\n    }\n}\n\nexport const BankRecAmls = {\n    ...EmbeddedListView,\n    Renderer: BankRecAmlsRenderer,\n};\n\nregistry.category(\"views\").add(\"bank_rec_amls_list_view\", BankRecAmls);\n", "import { KanbanRecordQuickCreate, KanbanQuickCreateController } from \"@web/views/kanban/kanban_record_quick_create\";\n\nexport class BankRecQuickCreateController extends KanbanQuickCreateController {\n    static template = \"account.BankRecQuickCreateController\";\n}\n\nexport class BankRecQuickCreate extends KanbanRecordQuickCreate {\n    static template = \"account.BankRecQuickCreate\";\n    static props = {\n        ...Object.fromEntries(Object.entries(KanbanRecordQuickCreate.props).filter(([k, v]) => k !== 'group')),\n        globalState: { type: Object, optional: true },\n    };\n    static components = { BankRecQuickCreateController };\n\n    /**\n    Overriden.\n    **/\n    async getQuickCreateProps(props) {\n        await super.getQuickCreateProps({...props,\n            group: {\n                resModel: props.globalState.quickCreateState.resModel,\n                context: props.globalState.quickCreateState.context,\n            }\n        });\n    }\n}\n", "/** @odoo-module **/\n\nimport { Record } from \"@web/model/relational_model/record\";\nimport { RelationalModel } from \"@web/model/relational_model/relational_model\";\nimport { parseServerValue } from \"@web/model/relational_model/utils\";\n\nexport class BankRecRecord extends Record {\n\n    /**\n     * override\n     * Track the changed field on lines.\n     */\n    async _update(changes) {\n        if(this.resModel === \"bank.rec.widget.line\"){\n            for(const fieldName of Object.keys(changes)){\n                this.model.lineIdsChangedField = fieldName;\n            }\n        }\n        return super._update(...arguments);\n    }\n\n    async updateToDoCommand(methodName, args, kwargs) {\n        this.dirty = true;\n\n        const onChangeFields = [\"todo_command\"];\n        const changes = {\n            todo_command: {\n                method_name: methodName,\n                args: args,\n                kwargs: kwargs,\n            },\n        };\n\n        const localChanges = this._getChanges(\n            { ...this._changes, ...changes },\n            { withReadonly: true }\n        );\n        const otherChanges = await this.model._onchange(this.config, {\n            changes: localChanges,\n            fieldNames: onChangeFields,\n            evalContext: this.evalContext,\n        });\n\n        const data = { ...this.data, ...changes };\n        for (const fieldName in otherChanges) {\n            data[fieldName] = parseServerValue(this.fields[fieldName], otherChanges[fieldName]);\n        }\n        const applyChanges = () => {\n            Object.assign(changes, this._parseServerValues(otherChanges, this.data));\n            if (Object.keys(changes).length > 0) {\n                this._applyChanges(changes);\n            }\n        };\n        return { data, applyChanges };\n    }\n\n    /**\n     * Bind an action to be called when a field on lines changed.\n     * @param {Function} callback: The action to call taking the changed field as parameter.\n     */\n    bindActionOnLineChanged(callback){\n        this._onUpdate = async () => {\n            const lineIdsChangedField = this.model.lineIdsChangedField;\n            if(lineIdsChangedField){\n                this.model.lineIdsChangedField = null;\n                await callback(lineIdsChangedField);\n            }\n        }\n    }\n}\n\nexport class BankRecRelationalModel extends RelationalModel{\n    setup(params, { action, dialog, notification, rpc, user, view, company }) {\n        super.setup(...arguments);\n        this.lineIdsChangedField = null;\n    }\n\n    load({ values }) {\n        this.root = this._createRoot(this.config, values);\n    }\n\n    getInitialValues() {\n        return this.root._getChanges(this.root.data, { withReadonly: true })\n    }\n}\nBankRecRelationalModel.Record = BankRecRecord;\n", "/** @odoo-module **/\n\nimport { ListController } from \"@web/views/list/list_controller\";\nimport { listView } from \"@web/views/list/list_view\";\n\nexport class BankRecEmbeddedListController extends ListController {\n    /** Remove the Export Cog **/\n    static template = \"account_accountant.BankRecEmbeddedListController\";\n}\n\n\nexport class BankRecWidgetFormEmbeddedListModel extends listView.Model {\n    setup(params, { action, dialog, notification, rpc, user, view, company }) {\n        super.setup(...arguments);\n        this.storedDomainString = null;\n    }\n\n    /**\n    * @override\n    * the list of AMLs don't need to be fetched from the server every time the form view is re-rendered.\n    * this disables the retrieval, while still ensuring that the search bar works.\n    */\n    async load(params = {}) {\n        const currentDomain = params.domain.toString();\n        if (currentDomain !== this.storedDomainString) {\n            this.storedDomainString = currentDomain;\n            return super.load(params);\n        }\n    }\n}\n\nexport const EmbeddedListView = {\n    ...listView,\n    Controller: BankRecEmbeddedListController,\n    Model: BankRecWidgetFormEmbeddedListModel,\n};\n", "/** @odoo-module **/\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class BankRecFinishButtons extends Component {\n    static template = \"account_accountant.BankRecFinishButtons\";\n    static props = {};\n\n    setup() {\n        this.breadcrumbs = useState(this.env.config.breadcrumbs);\n    }\n\n    getJournalFilter() {\n        // retrieves the searchModel's searchItem for the journal\n        return Object.values(this.searchModel.searchItems).filter(i => i.type == \"field\" && i.fieldName == \"journal_id\")[0];\n    }\n\n    get searchModel() {\n        return this.env.searchModel;\n    }\n\n    get otherFiltersActive() {\n        const facets = this.searchModel.facets;\n        const journalFilterItem = this.getJournalFilter();\n        for (const facet of facets) {\n            if (facet.groupId !== journalFilterItem.groupId) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    clearFilters() {\n        const facets = this.searchModel.facets;\n        const journalFilterItem = this.getJournalFilter();\n        for (const facet of facets) {\n            if (facet.groupId !== journalFilterItem.groupId) {\n                this.searchModel.deactivateGroup(facet.groupId);\n            }\n        }\n    }\n\n    breadcrumbBackOrDashboard() {\n        if (this.breadcrumbs.length > 1) {\n            this.env.services.action.restore();\n        } else {\n            this.env.services.action.doAction(\"account.open_account_journal_dashboard_kanban\", {clearBreadcrumbs: true});\n        }\n    }\n}\n", "/** @odoo-module **/\nimport { Component, onWillStart } from \"@odoo/owl\";\nimport { user } from \"@web/core/user\";\n\nexport class BankRecGlobalInfo extends Component {\n    static template = \"account_accountant.BankRecGlobalInfo\";\n    static props = {\n        journalId: { type: Number },\n        journalBalanceAmount: { type: String },\n    };\n\n    setup() {\n        this.hasGroupReadOnly = false;\n        onWillStart(async () => {\n            this.hasGroupReadOnly = await user.hasGroup(\"account.group_account_readonly\");\n        })\n    }\n\n    /** Open the bank reconciliation report. **/\n    actionOpenBankGL() {\n        this.env.methods.actionOpenBankGL(this.props.journalId);\n    }\n\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { scrollTo } from \"@web/core/utils/scrolling\";\nimport { getCurrency } from \"@web/core/currency\";\nimport { formatMonetary } from \"@web/views/fields/formatters\";\nimport { formatDate } from \"@web/core/l10n/dates\";\nimport { localization } from \"@web/core/l10n/localization\";\n\nimport { CallbackRecorder, useSetupAction } from \"@web/search/action_hook\";\nimport { RelationalModel } from \"@web/model/relational_model/relational_model\";\nimport { makeActiveField } from \"@web/model/relational_model/utils\";\nimport { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { KanbanController } from \"@web/views/kanban/kanban_controller\";\nimport { KanbanRenderer } from \"@web/views/kanban/kanban_renderer\";\nimport { KanbanRecord } from \"@web/views/kanban/kanban_record\";\n\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { Chatter } from \"@mail/chatter/web_portal/chatter\";\nimport { Many2ManyTagsField } from \"@web/views/fields/many2many_tags/many2many_tags_field\";\nimport { Many2OneField } from \"@web/views/fields/many2one/many2one_field\";\nimport { DateTimeField } from \"@web/views/fields/datetime/datetime_field\";\nimport { CharField } from \"@web/views/fields/char/char_field\";\nimport { AnalyticDistribution } from \"@analytic/components/analytic_distribution/analytic_distribution\";\nimport { TagsList } from \"@web/core/tags_list/tags_list\";\nimport { HtmlField } from \"@web_editor/js/backend/html_field\";\nimport { RainbowMan } from \"@web/core/effects/rainbow_man\";\nimport { Notebook } from \"@web/core/notebook/notebook\";\nimport { user } from \"@web/core/user\";\n\nimport { BankRecRelationalModel } from \"./bank_rec_record\";\nimport { BankRecMonetaryField } from \"./monetary_field_auto_signed_amount\";\nimport { BankRecViewEmbedder } from \"./view_embedder\";\nimport { BankRecRainbowContent } from \"./rainbowman_content\";\nimport { BankRecFinishButtons } from \"./finish_buttons\";\nimport { BankRecGlobalInfo } from \"./global_info\";\nimport { BankRecQuickCreate } from \"./bank_rec_quick_create\";\n\nimport { onPatched, useState, useEffect, useRef, useChildSubEnv, markRaw } from \"@odoo/owl\";\n\nexport class BankRecKanbanRecord extends KanbanRecord {\n    static template = \"account.BankRecKanbanRecord\";\n\n    setup(){\n        super.setup();\n        this.state = useState(this.env.methods.getState());\n    }\n\n    /** @override **/\n    getRecordClasses() {\n        let classes = `${super.getRecordClasses()} w-100 o_bank_rec_st_line`;\n        if (this.props.record.resId === this.state.bankRecStLineId) {\n            classes = `${classes} o_bank_rec_selected_st_line table-info`;\n        }\n        return classes;\n    }\n}\n\n\nexport class BankRecKanbanController extends KanbanController {\n    static template = \"account.BankRecoKanbanController\";\n    static props = {\n        ...KanbanController.props,\n        skipRestore: { optional: true },\n    };\n    static components = {\n        ...KanbanController.components,\n        Dropdown,\n        DropdownItem,\n        Many2OneField,\n        Many2ManyTagsField,\n        DateTimeField,\n        CharField,\n        AnalyticDistribution,\n        Chatter,\n        TagsList,\n        HtmlField,\n        BankRecMonetaryField,\n        Notebook,\n        BankRecViewEmbedder,\n    };\n\n    async setup() {\n        super.setup();\n\n        // ==================== INITIAL SETUP ====================\n\n        // Actions.\n        this.action = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.ui = useService(\"ui\");\n\n        // RelationalModel services.\n        this.relationalModelServices = Object.fromEntries(\n            RelationalModel.services.map((servName) => {\n                return [servName, useService(servName)];\n            })\n        );\n        this.relationalModelServices.orm = useService(\"orm\");\n\n        useChildSubEnv(this.getChildSubEnv());\n\n        // Mount the correct statement line when the search panel changed\n        this.env.searchModel.addEventListener(\n            \"update\",\n            () => {\n                this.model.bus.addEventListener(\n                    \"update\",\n                    this.onKanbanSearchModelChanged.bind(this),\n                    { once: true },\n                );\n            },\n        );\n\n        // ==================== STATE ====================\n\n        this.bankRecModel = null;\n\n        this.state = useState({\n            // BankRec.\n            bankRecStLineId: null,\n            bankRecRecordData: null,\n            bankRecEmbeddedViewsData: null,\n            bankRecNotebookPage: null,\n            bankRecClickedColumn: null,\n\n            // Global info.\n            journalId: null,\n            journalBalanceAmount: \"\",\n\n            // Asynchronous validation stuff.\n            lockedStLineIds: new Set(),\n            lockedAmlIds: new Set(),\n\n            quickCreateState : {\n                isVisible: false,\n                view: this.props.archInfo.quickCreateView,\n                context: this.props.context,\n            },\n        });\n\n        this.counter = {\n            // Counter state is separated as it should not be impacted by asynchronous changes, the last update is final.\n            startTime: null,\n            timeDiff: null,\n            count: null,\n        };\n\n        // When focusing the manual operations tab, mount the last line in edition automatically.\n        useEffect(\n            () => {\n                if(\n                    this.state.bankRecNotebookPage === \"manual_operations_tab\"\n                    && this.state.bankRecRecordData\n                    && !this.state.bankRecRecordData.form_index\n                ){\n                    this.actionMountLastLineInEdit();\n                }\n            },\n            () => [this.state.bankRecNotebookPage],\n        );\n\n        // ==================== EXPORT STATE ====================\n\n        this.viewRef = useRef(\"root\");\n\n        useSetupAction({\n            rootRef: this.viewRef,\n            getLocalState: () => {\n                const exportState = {};\n                if(this.bankRecModel.root.data.st_line_id){\n                    exportState.backupValues = Object.assign(\n                        {},\n                        this.state.bankRecEmbeddedViewsData,\n                        {\n                            bankRecStLineId: this.state.bankRecStLineId,\n                            initial_values: this.bankRecModel.getInitialValues(),\n                        },\n                    );\n                }\n                return exportState;\n            }\n        });\n\n        onPatched(() => {\n            if(\n                this.state.bankRecClickedColumn\n                && this.focusManualOperationField(this.state.bankRecClickedColumn)\n            ){\n                this.state.bankRecClickedColumn = null;\n            }\n        });\n\n        // ==================== LOCK SCREEN ====================\n\n        this.kanbanLock = false;\n        this.bankRecLock = false;\n        this.bankRecPromise = null;\n    }\n\n    // -----------------------------------------------------------------------------\n    // HELPERS CONCURRENCY\n    // -----------------------------------------------------------------------------\n\n    /**\n     * Execute the function passed as parameter initiated by the kanban.\n     * If some action is already processing by bankRecForm, it will wait until its completion.\n     * @param {Function} func: The action to execute.\n     */\n    async execProtectedAction(func){\n        if(this.kanbanLock){\n            return;\n        }\n        this.kanbanLock = true;\n        if(this.bankRecPromise){\n            await this.bankRecPromise;\n        }\n        await func();\n        this.kanbanLock = false;\n    }\n\n    /**\n     * Execute the function passed as parameter initiated by bankRecForm.\n     * If some concurrent actions are triggered by bankRecForm, the second one is ignored.\n     * @param {Function} func: The action to execute.\n     */\n    async execProtectedBankRecAction(func){\n        if(this.bankRecLock){\n            return;\n        }\n        this.bankRecLock = true;\n        try {\n            this.bankRecPromise = func();\n            await this.bankRecPromise;\n        } finally {\n            this.bankRecPromise = null;\n            this.bankRecLock = false;\n        }\n    }\n\n    // -----------------------------------------------------------------------------\n    // HELPERS STATE\n    // -----------------------------------------------------------------------------\n\n    getState(){\n        return this.state;\n    }\n\n    /**\n     * Since the kanban is driven by a reactive state for the additional stuff but by deep render\n     * in its base implementation, the code is turning crazy with rendering everytime a deep render\n     * is triggered. Indeed, notifying the model then changing things on the state could trigger a lot of\n     * mount/unmount of components implying useless rpc requests (when mounting multiple times an\n     * embedded list view for example).\n     * To avoid that, this method must be used everywhere to update only once the state and to delay\n     * the notify (itself triggering the deep render) after the update of the state.\n     * @param {Function} func: The action to execute taking the newState as parameter.\n     */\n    async withNewState(func){\n        const newState = {...this.state};\n        await func(newState);\n        if (newState.__commitChanges) {\n            newState.__commitChanges();\n            delete newState.__commitChanges;\n        }\n        Object.assign(this.state, newState);\n    }\n\n    // -----------------------------------------------------------------------------\n    // KANBAN OVERRIDES\n    // -----------------------------------------------------------------------------\n\n    /** override **/\n    get modelOptions() {\n        return {\n            ...super.modelOptions,\n            onWillStartAfterLoad: this.onWillStartAfterLoad.bind(this),\n        }\n    }\n\n    /**\n     * Define the sub environment allowing the sub-components to access some methods from\n     * the kanban.\n     */\n    getChildSubEnv(){\n        return {\n            // We don't care about subview states but we want to avoid them to record\n            // some callbacks in the BankRecKanbanController callback recorders passed\n            // by the action service.\n            __beforeLeave__: new CallbackRecorder(),\n            __getLocalState__: new CallbackRecorder(),\n            __getGlobalState__: new CallbackRecorder(),\n            __getContext__: new CallbackRecorder(),\n\n            // Accessible methods from sub-components.\n            methods: {\n                withNewState: this.withNewState.bind(this),\n                actionOpenBankGL: this.actionOpenBankGL.bind(this),\n                focusManualOperationField: this.focusManualOperationField.bind(this),\n                getState: this.getState.bind(this),\n                actionAddNewAml: this.actionAddNewAml.bind(this),\n                actionRemoveNewAml: this.actionRemoveNewAml.bind(this),\n                showRainbowMan: this.showRainbowMan.bind(this),\n                initReconCounter: this.initReconCounter.bind(this),\n                getCounterSummary: this.getCounterSummary.bind(this),\n                getRainbowManContentProps: this.getRainbowManContentProps.bind(this),\n                updateJournalState: this.updateJournalState.bind(this),\n            },\n        };\n    }\n\n    /** Called when the kanban is initialized. **/\n    async onWillStartAfterLoad(){\n        // Fetch groups.\n        this.hasGroupAnalyticAccounting = await user.hasGroup(\"analytic.group_analytic_accounting\");\n        this.hasGroupReadOnly = await user.hasGroup(\"account.group_account_readonly\");\n\n\n        // Prepare bankRecoModel.\n        await this.initBankRecModel();\n\n        let stLineId = null;\n        let backupValues = null;\n\n        // Try to restore.\n        if(this.props.state && this.props.state.backupValues && !this.props.skipRestore){\n            const backupStLineId = this.props.state.backupValues.bankRecStLineId;\n            if(this.model.root.records.find(x => x.resId === backupStLineId)){\n                stLineId = backupStLineId;\n                backupValues = this.props.state.backupValues;\n            }\n        }\n\n        // Find the next transaction to mount.\n        if(!stLineId){\n            stLineId = this.getNextAvailableStLineId();\n        }\n\n        await this.withNewState(async (newState) => {\n\n            // Mount the transaction if any.\n            if(stLineId){\n                await this._mountStLineInEdit(newState, stLineId, backupValues);\n            }else{\n                await this.updateJournalState(newState);\n            }\n\n            this.initReconCounter();\n        });\n    }\n\n    /** Called when the something changed in the kanban search model. **/\n    async onKanbanSearchModelChanged(){\n        await this.execProtectedAction(async () => {\n            await this.withNewState(async (newState) => {\n                if(this.model.root.records.find(x => x.resId === newState.bankRecStLineId)){\n                    return;\n                }\n\n                const nextStLineId = this.getNextAvailableStLineId();\n                await this._mountStLineInEdit(newState, nextStLineId);\n            });\n        });\n    }\n\n    /**\n    Method called when the user clicks on a card.\n    **/\n    async openRecord(record, mode) {\n        const currentStLineId = this.bankRecModel ? this.bankRecModel.root.data.st_line_id[0] : null;\n        const isSameStLineId = currentStLineId && currentStLineId === record.resId;\n        if (isSameStLineId) {\n            return;\n        }\n        await this.execProtectedAction(async () => {\n            await this.withNewState(async (newState) => {\n                await this._mountStLineInEdit(newState, record.resId);\n            });\n        });\n    }\n\n    /**\n    Method called when the user changes the search pager.\n    **/\n    async onUpdatedPager() {\n        await this.execProtectedAction(async () => {\n            await this.withNewState(async (newState) => {\n                const nextStLineId = this.getNextAvailableStLineId();\n                await this._mountStLineInEdit(newState, nextStLineId);\n            });\n        });\n    }\n\n    onPageUpdate(page) {\n        if (this.state.bankRecNotebookPage !== page) {\n            this.state.bankRecNotebookPage = page;\n        }\n    }\n\n    /**\n    Overriden.\n    **/\n    get canQuickCreate() {\n        return true;\n    }\n\n    /**\n    Overriden.\n    **/\n    createRecord() {\n        const { onCreate } = this.props.archInfo;\n        const searchModel = this.env.searchModel;\n        const journalFilter = Object.values(searchModel.searchItems).filter(i => i.type == \"field\" && i.fieldName == \"journal_id\")[0];\n\n        // If there are no records, deactivate all filters except the journal one.\n        if (!this.model.root.records.length) {\n            searchModel.facets.forEach(facet => {\n                if(facet.groupId !== journalFilter.groupId)\n                    searchModel.deactivateGroup(facet.groupId)\n            });\n        }\n\n        if (onCreate === \"quick_create\" && this.canQuickCreate) {\n            this.state.quickCreateState = {\n                ...this.state.quickCreateState,\n                isVisible: true,\n                resModel: this.props.resModel,\n                model: this.model,\n            };\n        }\n    }\n\n    // -----------------------------------------------------------------------------\n    // NEXT STATEMENT LINE\n    // -----------------------------------------------------------------------------\n\n    /**\n    Get the next eligible statement line for reconciliation.\n    @param afterStLineId:   An optional id of a statement line indicating we want the\n                            next available line after this one.\n    @param records:         An optional list of records.\n    **/\n    getNextAvailableStLineId(afterStLineId=null, records=null) {\n        const stLines = this.model.root.records;\n\n        // Find all available records that need to be validated.\n        const isRecordReady = (x) => (!x.data.is_reconciled || !x.data.checked);\n        let waitBeforeReturn = Boolean(afterStLineId);\n        let availableRecordIds = [];\n        for (const stLine of (records || stLines)) {\n            if (waitBeforeReturn) {\n                if (stLine.resId === afterStLineId) {\n                    waitBeforeReturn = false;\n                }\n            } else if (isRecordReady(stLine)) {\n                availableRecordIds.push(stLine.resId);\n            }\n        }\n\n        // No records left, focus the first record instead. This behavior is mainly there when clicking on \"View\" from\n        // the list view to show an already reconciled line.\n        if (!availableRecordIds.length && stLines.length === 1) {\n            availableRecordIds = [stLines[0].resId];\n        }\n\n        if (availableRecordIds.length){\n            return availableRecordIds[0];\n        } else if(stLines.length) {\n            return stLines[0].resId;\n        } else {\n            return null;\n        }\n    }\n\n    /**\n    Mount the statement line passed as parameter into the edition widget.\n    @param stLineId: The id of the statement line to mount.\n    **/\n    async _mountStLineInEdit(newState, stLineId, initialData = null) {\n        newState.bankRecStLineId = stLineId;\n        let data = {};\n        if (initialData) {\n            // Restore an existing transaction.\n            data = await this.onchange(newState, \"restore_st_line_data\", [initialData]);\n            const bankRecEmbeddedViewsData = data.return_todo_command;\n            for (const [key, value] of Object.entries(bankRecEmbeddedViewsData)) {\n                if (value instanceof Object) {\n                    bankRecEmbeddedViewsData[key] = Object.assign(\n                        {},\n                        initialData[key] || {},\n                        value\n                    );\n                } else {\n                    bankRecEmbeddedViewsData[key] = value;\n                }\n            }\n            newState.bankRecEmbeddedViewsData = markRaw(bankRecEmbeddedViewsData);\n            newState.bankRecNotebookPage = null;\n        } else if (stLineId) {\n            // Mount a new transaction.\n            data = await this.onchange(newState, \"mount_st_line\", [stLineId]);\n            const bankRecEmbeddedViewsData = data.return_todo_command\n            newState.bankRecEmbeddedViewsData = bankRecEmbeddedViewsData;\n            newState.bankRecNotebookPage = null;\n        } else {\n            // No transaction mounted.\n            newState.bankRecNotebookPage = null;\n            newState.bankRecRecordData = null;\n        }\n\n        // Refresh balance.\n        await this.updateJournalState(newState, data);\n\n        // Scroll to the next kanban card iff the view is mounted, a line is selected  and the kanban\n        // card is in the view (cannot use .o_bank_rec_selected_st_line as the dom may not be patched yet)\n        if (stLineId && this.viewRef.el) {\n            const selectedKanbanCardEl = this.viewRef.el.querySelector(\n                `[st-line-id=\"${stLineId}\"]`\n            );\n            if (selectedKanbanCardEl) {\n                scrollTo(selectedKanbanCardEl, {});\n            }\n        }\n    }\n\n    /**\n    Mount the statement line passed as parameter into the edition widget.\n    @param stLineId: The id of the statement line to mount.\n    **/\n    async mountStLineInEdit(stLineId, initialData=null){\n        await this.withNewState(async (newState) => {\n            await this._mountStLineInEdit(newState, stLineId, initialData);\n        });\n    }\n\n    // -----------------------------------------------------------------------------\n    // BANK_REC_RECORD\n    // -----------------------------------------------------------------------------\n\n    async initBankRecModel(){\n        const initialData = await this.orm.call(\n            \"bank.rec.widget\",\n            \"fetch_initial_data\",\n        );\n\n        // Services.\n        function makeActiveFields(fields) {\n            const activeFields = {};\n            for (const fieldName in fields) {\n                const field = fields[fieldName];\n                activeFields[fieldName] = makeActiveField({ onChange: field.onChange});\n                if (field.relatedFields) {\n                    activeFields[fieldName].related = {\n                        fields: field.relatedFields,\n                        activeFields: makeActiveFields(field.relatedFields),\n                    }\n                }\n            }\n            return activeFields;\n        }\n        const activeFields = makeActiveFields(initialData.fields);\n        this.bankRecModel = new BankRecRelationalModel(\n            this.env,\n            {\n                config: {\n                    resModel: \"bank.rec.widget\",\n                    fields: initialData.fields,\n                    activeFields,\n                    mode: \"edit\",\n                    isMonoRecord: true,\n                }\n            },\n            this.relationalModelServices,\n        );\n\n        // Initial loading.\n        await this.bankRecModel.load({\n            values: initialData.initial_values,\n        });\n\n        const record = this.bankRecModel.root;\n        record.bindActionOnLineChanged(async (changedField) => {\n            await this.actionLineChanged(changedField);\n        });\n    }\n\n    getBankRecRecordLineInEdit(){\n        const data = this.state.bankRecRecordData;\n        const lineIndex = data.form_index;\n        return data.line_ids.records.find((x) => x.data.index === lineIndex);\n    }\n\n    // -----------------------------------------------------------------------------\n    // GLOBAL INFO\n    // -----------------------------------------------------------------------------\n\n    async updateJournalState(newState, data = {}) {\n        // Find the journal.\n        let journalId = null;\n        const stLineJournalId = data.st_line_journal_id;\n        if(stLineJournalId){\n            journalId = stLineJournalId[0];\n        }else if(this.model.root.records.length){\n            journalId = this.model.root.records[0].data.journal_id[0];\n        }else{\n            journalId = this.props.context.default_journal_id;\n        }\n        newState.journalId = journalId;\n        const values = await this.orm.call(\n            \"bank.rec.widget\",\n            \"collect_global_info_data\",\n            [journalId],\n        );\n        newState.journalBalanceAmount = values.balance_amount;\n    }\n\n    // -----------------------------------------------------------------------------\n    // COUNTER / RAINBOWMAN\n    // -----------------------------------------------------------------------------\n\n    /** Reset the timing and reconciliation counter */\n    initReconCounter() {\n        this.counter.startTime = luxon.DateTime.now();\n        this.counter.timeDiff = null;\n        this.counter.count = 0;\n    }\n\n    /** Increment the timing and reconciliation counter */\n    incrementReconCounter() {\n        const start = this.counter.startTime.set({millisecond: 0});\n        const end = luxon.DateTime.now().set({millisecond: 0});\n        this.counter.timeDiff = end.diff(start, \"seconds\");\n        this.counter.count += 1;\n    }\n\n    showRainbowMan(){\n        return this.counter.count > 0;\n    }\n\n    getCounterSummary() {\n        const diff = this.counter.timeDiff;\n        const total = this.counter.count;\n        const diffInSeconds = diff.seconds;\n        let units = [\"seconds\"];\n        if (diffInSeconds > 60) {\n            units.unshift(\"minutes\");\n        }\n        if (diffInSeconds > 3600) {\n            units.unshift(\"hours\");\n        }\n        return {\n            counter: total,\n            secondsPerTransaction: Math.round(diffInSeconds / total),\n            formattedDuration: diff.toFormat(localization.timeFormat.replace(/HH/, \"hh\")),\n            humanDuration: diff.shiftTo(...units).toHuman(),\n        }\n    }\n\n    getRainbowManContentProps(){\n        return {\n            fadeout: \"no\",\n            message: \"\",\n            imgUrl: \"/web/static/img/smile.svg\",\n            Component: BankRecRainbowContent,\n            close: () => {},\n        }\n    }\n\n    // -----------------------------------------------------------------------------\n    // HELPERS BANK_REC_RECORD\n    // -----------------------------------------------------------------------------\n\n    async moveToNextLine(newState){\n        const records = this.model.root.records;\n        const counter = newState.counter;\n        await this.model.root.load();\n\n        const nextStLineId = this.getNextAvailableStLineId(newState.bankRecStLineId, records);\n        if(nextStLineId != newState.bankRecStLineId){\n            await this._mountStLineInEdit(newState, nextStLineId);\n        }\n        newState.counter = counter;\n        newState.__kanbanNotify = true;\n    }\n\n    formatMonetaryField(amount, currencyId){\n        const currencyDigits = getCurrency(currencyId)?.digits;\n        return formatMonetary(amount, {\n            digits: currencyDigits,\n            currencyId: currencyId,\n        });\n    }\n\n    isMonetaryZero(amount, currencyId){\n        const currencyDigits = getCurrency(currencyId)?.digits;\n        return Number(amount.toFixed(currencyDigits ? currencyDigits[1] : 2)) === 0;\n    }\n\n    formatDateField(date){\n        return formatDate(date);\n    }\n\n    async onchange(newState, methodName, args, kwargs){\n        const record = this.bankRecModel.root;\n        const { data, applyChanges } = await record.updateToDoCommand(methodName, args, kwargs);\n\n        newState.__commitChanges = () => {\n            applyChanges();\n            newState.bankRecRecordData = record.data;\n            newState.__bankRecRecordNotify = true;\n        };\n        return data;\n    }\n\n    getOne2ManyColumns() {\n        const data = this.state.bankRecRecordData;\n        let lineIdsRecords = data.line_ids.records;\n\n        // Prepare columns.\n        let columns = [\n            [\"date\", _t(\"Date\")],\n            [\"partner\", _t(\"Partner\")],\n        ];\n        if(lineIdsRecords.some((x) => Boolean(Object.keys(x.data.analytic_distribution).length))){\n            columns.push([\"analytic_distribution\", _t(\"Analytic\")]);\n        }\n        if(lineIdsRecords.some((x) => x.data.tax_ids.records.length)){\n            columns.push([\"taxes\", _t(\"Taxes\")]);\n        }\n        if(lineIdsRecords.some((x) => x.data.currency_id[0] !== data.company_currency_id[0])){\n            columns.push([\"amount_currency\", _t(\"Amount in Currency\")], [\"currency\", _t(\"Currency\")]);\n        }\n        if (this.hasGroupReadOnly) {\n            columns.unshift([\"account\", _t(\"Account\")]);\n            columns.push(\n                [\"debit\", _t(\"Debit\")],\n                [\"credit\", _t(\"Credit\")],\n                [\"__trash\", \"\"],\n            );\n        } else {\n            columns.push(\n                [\"balance\", _t(\"Amount\")],\n                [\"__trash\", \"\"],\n            );\n        }\n\n        return columns;\n    }\n\n    getKey(lineData) {\n        return `${lineData.index} ${JSON.stringify(lineData.analytic_distribution)}`;\n    }\n\n    checkBankRecLineRequiredField(line, invalidFields, fieldName, condition){\n        if(!line.data[fieldName] && (!condition || condition())){\n            invalidFields.push(fieldName);\n        }\n    }\n\n    getBankRecLineInvalidFields(line){\n        const invalidFields = [];\n        this.checkBankRecLineRequiredField(line, invalidFields, \"account_id\");\n        this.checkBankRecLineRequiredField(line, invalidFields, \"date\", () => line.data.flag === \"liquidity\");\n        return invalidFields;\n    }\n\n    checkBankRecLinesInvalidFields(data){\n        return data.line_ids.records.filter((l) => this.getBankRecLineInvalidFields(l).length > 0).length === 0;\n    }\n\n    notebookAmlsListViewProps(){\n        const initParams = this.state.bankRecEmbeddedViewsData.amls;\n        const ctx = initParams.context;\n        const suspenseLine = this.state.bankRecRecordData.line_ids.records.filter((l) => l.data.flag == \"auto_balance\");\n        if (suspenseLine.length) {\n            // Change the sort order of the AML's in the list view based on the amount of the suspense line\n            // This is done from JS instead of python because the embedded_views_data is only prepared when selecting\n            // a statement line, and not after mounting an AML that would change the auto_balance value (suspense line)\n            ctx['preferred_aml_value'] = suspenseLine[0].data.amount_currency * -1;\n            ctx['preferred_aml_currency_id'] = suspenseLine[0].data.currency_id[0];\n        }\n        return {\n            type: \"list\",\n            noBreadcrumbs: true,\n            resModel: \"account.move.line\",\n            searchMenuTypes: [\"filter\", \"favorite\"],\n            domain: initParams.domain,\n            dynamicFilters: initParams.dynamic_filters,\n            context: ctx,\n            allowSelectors: false,\n            searchViewId: false, // little hack: force to load the search view info\n            globalState: initParams.exportState,\n            loadIrFilters: true,\n        }\n    }\n\n    /**\n    Focus the field corresponding to the column name passed as parameter inside the\n    manual operation page.\n    **/\n    focusManualOperationField(clickedColumn){\n        // Focus the field corresponding to the clicked column.\n        if (['debit', 'credit'].includes(clickedColumn)) {\n            if (this.focusElement(\"div[name='balance'] input\")) {\n                return true;\n            }\n            if (this.focusElement(\"div[name='amount_currency'] input\")) {\n                return true;\n            }\n        }\n\n        if (this.focusElement(`div[name='${clickedColumn}'] input`)) {\n            return true;\n        }\n        if (this.focusElement(`input[name='${clickedColumn}']`)) {\n            return true;\n        }\n        return false;\n    }\n\n    /** Helper to find the corresponding field to focus inside the DOM. **/\n    focusElement(selector) {\n        const inputEl = this.viewRef.el.querySelector(selector);\n        if (!inputEl) {\n            return false;\n        }\n\n        if (inputEl.tagName === \"INPUT\") {\n            inputEl.focus();\n            inputEl.select();\n        } else {\n            inputEl.focus();\n        }\n        return true;\n    }\n\n    // -----------------------------------------------------------------------------\n    // RPC\n    // -----------------------------------------------------------------------------\n\n    async actionOpenBankGL(journalId) {\n        const actionData = await this.orm.call(\n            \"account.journal\",\n            \"action_open_bank_balance_in_gl\",\n            [journalId],\n        );\n        this.action.doAction(actionData);\n    }\n\n    async actionRemoveLine(line){\n        await this.execProtectedBankRecAction(async () => {\n            await this.withNewState(async (newState) => {\n                await this.onchange(newState, \"remove_line\", [line.data.index]);\n\n                if(newState.bankRecNotebookPage === \"manual_operations_tab\"){\n                    newState.bankRecNotebookPage = \"amls_tab\";\n                }\n            });\n        });\n    }\n\n    async actionSelectRecoModel(recoModel){\n        await this.execProtectedBankRecAction(async () => {\n            await this.withNewState(async (newState) => {\n                const data = newState.bankRecRecordData;\n                if(recoModel.resId == data.selected_reco_model_id.id){\n                    return;\n                }\n                const { return_todo_command: actionData } = await this.onchange(newState, \"select_reconcile_model\", [recoModel.resId])\n                if(actionData){\n                    this.action.doAction(actionData);\n                }\n            });\n        });\n    }\n\n    actionCreateRecoModel(){\n        this.execProtectedBankRecAction(async () => {\n            const journalId = this.state.bankRecRecordData.st_line_journal_id[0];\n            const lines = this.state.bankRecRecordData.line_ids.records;\n\n            const defaultLineIds = [];\n            let balance = lines.filter(line => line.data.flag === \"liquidity\")[0].data.balance\n            if(!this.isMonetaryZero(balance, this.state.bankRecRecordData.company_currency_id[0])){\n                for (const line of lines) {\n                    const data = line.data;\n                    if (![\"manual\", \"aml\"].includes(data.flag)){\n                        continue;\n                    }\n\n                    defaultLineIds.push([0, 0, {\n                        label: data.name,\n                        account_id: data.account_id[0],\n                        tax_ids: [[6, 0, data.tax_ids.currentIds]],\n                        amount_type: \"percentage\",\n                        amount_string: ((-data.balance / balance) * 100).toFixed(5),\n                    }]);\n                    balance += data.balance;\n                }\n            }\n\n            this.action.doAction({\n                type: \"ir.actions.act_window\",\n                res_model: \"account.reconcile.model\",\n                views: [[false, \"form\"]],\n                target: \"current\",\n                context: {\n                    default_match_journal_ids: [journalId],\n                    default_line_ids: defaultLineIds,\n                    default_to_check: !this.state.bankRecRecordData.checked,\n                },\n            });\n        });\n    }\n\n    actionViewRecoModels(){\n        this.execProtectedBankRecAction(async () => {\n            this.action.doAction(\"account.action_account_reconcile_model\");\n        });\n    }\n\n    async _actionValidate(newState){\n        const { return_todo_command: result } = await this.onchange(newState, \"validate\");\n        if(result.done){\n            this.incrementReconCounter();\n            await this.moveToNextLine(newState);\n        }\n        return result;\n    }\n\n    async actionValidate(){\n        await this.execProtectedBankRecAction(async () => {\n            await this.withNewState(async (newState) => {\n                await this._actionValidate(newState);\n            });\n        });\n    }\n\n    async actionReset(){\n        await this.execProtectedBankRecAction(async () => {\n            await this.withNewState(async (newState) => {\n                const { return_todo_command: result } = await this.onchange(newState, \"reset\");\n\n                if(result.done){\n                    await this.model.root.load();\n\n                    const stLineId = newState.bankRecStLineId;\n                    if(!stLineId){\n                        return;\n                    }\n\n                    const records = this.model.root.records;\n                    if(!records.length){\n                        // The transaction is not longer available on the kanban.\n                        newState.bankRecStLineId = null;\n                        newState.bankRecNotebookPage = null;\n                        newState.bankRecRecordData = null;\n                    }else if(!records.find((x) => x.resId === stLineId)){\n                        // Move to the next available transaction.\n                        const nextStLineId = this.getNextAvailableStLineId(stLineId);\n                        await this._mountStLineInEdit(newState, nextStLineId);\n                    }\n\n                    if(newState.bankRecNotebookPage != \"amls_tab\"){\n                        newState.bankRecNotebookPage = \"amls_tab\";\n                    }\n\n                    newState.__kanbanNotify = true;\n                }\n            });\n        });\n    }\n\n    async actionToCheck(){\n        await this.execProtectedBankRecAction(async () => {\n            await this.withNewState(async (newState) => {\n                const { return_todo_command: result } = await this.onchange(newState, \"to_check\");\n                if(result.done){\n                    await this.moveToNextLine(newState);\n                }\n            });\n        });\n    }\n\n    async actionSetAsChecked(){\n        await this.execProtectedBankRecAction(async () => {\n            await this.withNewState(async (newState) => {\n                const data = await this.onchange(newState, \"set_as_checked\");\n                const result = data.return_todo_command;\n                if(result.done && data.state === \"reconciled\"){\n                    await this.moveToNextLine(newState);\n                }else{\n                    await this.model.root.load();\n                    newState.__kanbanNotify = true;\n                }\n            });\n        });\n    }\n\n    async actionAddNewAml(amlId){\n        await this.execProtectedBankRecAction(async () => {\n            await this.withNewState(async (newState) => {\n                await this.onchange(newState, \"add_new_aml\", [amlId]);\n            });\n        });\n    }\n\n    async actionRemoveNewAml(amlId){\n        await this.execProtectedBankRecAction(async () => {\n            await this.withNewState(async (newState) => {\n                await this.onchange(newState, \"remove_new_aml\", [amlId]);\n             });\n        });\n    }\n\n    async _actionMountLineInEdit(newState, line){\n        const data = newState.bankRecRecordData;\n        const currentLineIndex = data.form_index;\n        if(line.data.index != currentLineIndex){\n            // Mount the line in edition on the form.\n            await this.onchange(newState, \"mount_line_in_edit\", [line.data.index]);\n        }\n\n        if(newState.bankRecNotebookPage !== \"manual_operations_tab\"){\n            newState.bankRecNotebookPage = \"manual_operations_tab\";\n        }\n    }\n\n    async actionMountLineInEdit(line){\n        await this.execProtectedBankRecAction(async () => {\n            await this.withNewState(async (newState) => {\n                await this._actionMountLineInEdit(newState, line);\n            });\n        });\n    }\n\n    async actionMountLastLineInEdit(){\n        await this.execProtectedBankRecAction(async () => {\n            await this.withNewState(async (newState) => {\n                const data = newState.bankRecRecordData;\n                const line = data.line_ids.records.at(-1);\n                await this._actionMountLineInEdit(newState, line);\n            });\n        });\n    }\n\n    async postprocessLineChangedReturnTodoCommand(newState, data) {\n        const todo = data.return_todo_command;\n        if(!todo){\n            return;\n        }\n        if(todo.reset_record){\n            await this.model.root.load();\n            newState.__kanbanNotify = true;\n        }\n        if(todo.reset_global_info){\n            await this.updateJournalState(newState, data);\n        }\n    }\n\n    async actionLineChanged(fieldName){\n        await this.execProtectedBankRecAction(async () => {\n            const line = this.getBankRecRecordLineInEdit();\n            await this.withNewState(async (newState) => {\n                if(line){\n                    const data = await this.onchange(newState, \"line_changed\", [line.data.index, fieldName]);\n                    await this.postprocessLineChangedReturnTodoCommand(newState, data);\n                }\n            });\n        });\n    }\n\n    async actionSetPartnerReceivableAccount(){\n        await this.execProtectedBankRecAction(async () => {\n            const line = this.getBankRecRecordLineInEdit();\n            await this.withNewState(async (newState) => {\n                if(line){\n                    const data = await this.onchange(newState, \"line_set_partner_receivable_account\", [line.data.index])\n                    await this.postprocessLineChangedReturnTodoCommand(newState, data);\n                }\n            });\n        });\n    }\n\n    async actionSetPartnerPayableAccount(){\n        await this.execProtectedBankRecAction(async () => {\n            const line = this.getBankRecRecordLineInEdit();\n            await this.withNewState(async (newState) => {\n                if(line){\n                    const data = await this.onchange(newState, \"line_set_partner_payable_account\", [line.data.index])\n                    await this.postprocessLineChangedReturnTodoCommand(newState, data);\n                }\n            });\n        });\n    }\n\n    async actionRedirectToSourceMove(line){\n        await this.execProtectedBankRecAction(async () => {\n            await this.withNewState(async (newState) => {\n                const { return_todo_command: actionData } = await this.onchange(newState, \"redirect_to_move\", [line.data.index])\n                if(actionData){\n                    this.action.doAction(actionData);\n                }\n            });\n        });\n    }\n\n    async actionApplyLineSuggestion(){\n        await this.execProtectedBankRecAction(async () => {\n            const line = this.getBankRecRecordLineInEdit();\n            await this.withNewState(async (newState) => {\n                if(line){\n                    await this.onchange(newState, \"apply_line_suggestion\", [line.data.index])\n                }\n            });\n        });\n    }\n\n    async handleLineClicked(ev, line){\n        const lineIndexBeforeClick = this.state.bankRecRecordData.form_index;\n        await this.actionMountLineInEdit(line);\n\n        let clickedColumn = null;\n        const target = ev.target.tagName === \"TD\" ? ev.target : ev.target.closest(\"td\");\n        if (target?.attributes && target.attributes.field) {\n            clickedColumn = target.attributes.field.value;\n        }\n\n        // Track the clicked column to focus automatically the corresponding field on the manual operations page.\n        // In case we did not change the selected line we directly focus the corresponding field.\n        if(clickedColumn){\n            if(lineIndexBeforeClick === line.data.index) {\n                this.focusManualOperationField(clickedColumn);\n                this.state.bankRecClickedColumn = null;\n            } else {\n                this.state.bankRecClickedColumn = clickedColumn;\n            }\n        }\n    }\n\n    async handleSuggestionHtmlClicked(ev){\n        if (ev.target.tagName === \"BUTTON\"){\n            const buttonName = ev.target.attributes && ev.target.attributes.name ? ev.target.attributes.name.value : null;\n            if (!buttonName) {\n                return;\n            }\n\n            if (buttonName === \"action_redirect_to_move\"){\n                const line = this.getBankRecRecordLineInEdit();\n                await this.actionRedirectToSourceMove(line);\n            } else if (buttonName === \"action_apply_line_suggestion\"){\n                await this.actionApplyLineSuggestion();\n            }\n        }\n    }\n\n}\n\nexport class BankRecKanbanRenderer extends KanbanRenderer {\n    static template = \"account.BankRecKanbanRenderer\";\n    static components = {\n        ...KanbanRenderer.components,\n        KanbanRecord: BankRecKanbanRecord,\n        RainbowMan,\n        BankRecFinishButtons,\n        BankRecGlobalInfo,\n        BankRecQuickCreate,\n    };\n    setup() {\n        super.setup();\n        this.globalState = useState(this.env.methods.getState());\n        this.action = useService(\"action\");\n    }\n\n    /**\n    Prepares a list of statements based on the statement_id of the bank statement line records.\n    Statements are only displayed above the first line of the statement (all lines might not be visible in the kanban)\n    **/\n    groups() {\n        const { list } = this.props;\n        let statementGroups = [];\n        for (const record of list.records) {\n            let lastItem = statementGroups.slice(-1);\n            let statementId = record.data.statement_id && record.data.statement_id[0];\n            if (statementId && (!lastItem.length || lastItem[0].id != statementId)) {\n                statementGroups.push({\n                    id: statementId,\n                    name: record.data.statement_name,\n                    balance: formatMonetary(record.data.statement_balance_end_real, {currencyId: record.data.currency_id[0]}),\n                });\n            }\n        }\n        return statementGroups;\n    }\n\n    openStatementDialog(statementId) {\n        const action = {\n            type: \"ir.actions.act_window\",\n            res_model: \"account.bank.statement\",\n            res_id: statementId,\n            views: [[false, \"form\"]],\n            target: \"current\",\n            context: {\n                form_view_ref: \"account_accountant.view_bank_statement_form_bank_rec_widget\",\n            },\n        };\n\n        this.action.doAction(action);\n    }\n\n    // -----------------------------------------------------------------------------\n    // QUICK CREATE CALLBACKS\n    // -----------------------------------------------------------------------------\n\n    /**\n    Overriden.\n    **/\n    cancelQuickCreate() {\n        this.globalState.quickCreateState.isVisible = false;\n    }\n\n    /**\n    Overriden.\n    **/\n    validateQuickCreate(_recordId, mode) {\n        this.globalState.quickCreateState.model.load()\n        if (mode === \"add_close\") {\n            this.globalState.quickCreateState.isVisible = false;\n        }\n    }\n}\n\nexport const BankRecKanbanView = {\n    ...kanbanView,\n    Controller: BankRecKanbanController,\n    Renderer: BankRecKanbanRenderer,\n    searchMenuTypes: [\"filter\", \"favorite\"],\n};\n\nregistry.category(\"views\").add('bank_rec_widget_kanban', BankRecKanbanView);\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { listView } from \"@web/views/list/list_view\";\nimport { ListController } from \"@web/views/list/list_controller\";\n\nimport { useChildSubEnv } from \"@odoo/owl\";\n\nexport class BankRecListController extends ListController {\n\n    setup() {\n        super.setup(...arguments);\n\n        this.skipKanbanRestore = {};\n\n        useChildSubEnv({\n            skipKanbanRestoreNeeded: (stLineId) => this.skipKanbanRestore[stLineId],\n        });\n    }\n\n    /**\n      * Override\n      * Don't allow bank_rec_form to be restored with previous values since the statement line has changed.\n      */\n    async onRecordSaved(record) {\n        this.skipKanbanRestore[record.resId] = true;\n        return super.onRecordSaved(...arguments);\n    }\n\n}\n\nexport const bankRecListView = {\n    ...listView,\n    Controller: BankRecListController,\n}\n\nregistry.category(\"views\").add(\"bank_rec_list\", bankRecListView);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { Component } from \"@odoo/owl\";\n\nexport class ListViewSwitcher extends Component {\n    static template = \"account_accountant.ListViewSwitcher\";\n    static props = standardFieldProps;\n\n    setup() {\n        this.action = useService(\"action\");\n    }\n\n    /** Called when the Match/View button is clicked. **/\n    switchView() {\n        // Add a new search facet to restrict the results to the selected statement line.\n        const searchItem = Object.values(this.env.searchModel.searchItems).find((i) => i.fieldName === \"statement_line_id\");\n        const stLineId = this.props.record.resId;\n        const autocompleteValue = {\n            label: this.props.record.data.move_id[1],\n            operator: \"=\",\n            value: stLineId,\n        }\n        this.env.searchModel.addAutoCompletionValues(searchItem.id, autocompleteValue);\n\n        // Switch to the kanban.\n        this.action.switchView(\"kanban\", { skipRestore: this.env.skipKanbanRestoreNeeded(stLineId) });\n    }\n\n    /** Give the button's label for the current record. **/\n    get buttonLabel() {\n        return this.props.record.data.is_reconciled ? _t(\"View\") : _t(\"Match\");\n    }\n}\n\nregistry.category(\"fields\").add('bank_rec_list_view_switcher', {component: ListViewSwitcher});\n", "/** @odoo-module **/\nimport { registry } from \"@web/core/registry\";\nimport { Many2OneField, many2OneField } from \"@web/views/fields/many2one/many2one_field\";\n\nexport class BankRecMany2OneMultiID extends Many2OneField {\n\n    get Many2XAutocompleteProps() {\n        const props = super.Many2XAutocompleteProps;\n        if (this.props.record.selected && this.props.record.model.multiEdit) {\n            props.context.active_ids = this.env.model.root.selection.map((r) => r.resId);\n        }\n        return props;\n    }\n}\n\nexport const bankRecMany2OneMultiID = {\n    ...many2OneField,\n    component: BankRecMany2OneMultiID,\n};\n\nregistry.category(\"fields\").add(\"bank_rec_list_many2one_multi_id\", bankRecMany2OneMultiID);\n", "/** @odoo-module **/\nimport { MonetaryField, monetaryField } from \"@web/views/fields/monetary/monetary_field\";\n\nexport class BankRecMonetaryField extends MonetaryField{\n    static template = \"account_accountant.BankRecMonetaryField\";\n    static props = {\n        ...MonetaryField.props,\n        hasForcedNegativeValue: { type: Boolean },\n    };\n\n    /** Override **/\n    get inputOptions(){\n        const options = super.inputOptions;\n        const parse = options.parse;\n        options.parse = (value) => {\n            let parsedValue = parse(value);\n            if (this.props.hasForcedNegativeValue) {\n                parsedValue = -Math.abs(parsedValue);\n            }\n            return parsedValue;\n        };\n        return options;\n    }\n\n    /** Override **/\n    get value() {\n        let value = super.value;\n        if(this.props.hasForcedNegativeValue){\n            value = Math.abs(value);\n        }\n        return value;\n    }\n}\n\nexport const bankRecMonetaryField = {\n    ...monetaryField,\n    component: BankRecMonetaryField,\n};\n", "/** @odoo-module **/\nimport { BankRecFinishButtons } from \"./finish_buttons\";\nimport { Component, onWillUnmount } from \"@odoo/owl\";\n\nexport class BankRecRainbowContent extends Component {\n    static template = \"account_accountant.BankRecRainbowContent\";\n    static components = { BankRecFinishButtons };\n    static props = {};\n\n    setup() {\n        onWillUnmount(() => {\n            this.env.methods.initReconCounter();\n        });\n    }\n}\n", "/** @odoo-module */\nimport { View } from \"@web/views/view\";\nimport { Component, useSubEnv } from \"@odoo/owl\";\n\nexport class BankRecViewEmbedder extends Component {\n    static props = [\"viewProps\"];\n    static template = \"account_accountant.BankRecViewEmbedder\";\n    static components = { View };\n\n    setup() {\n        // Little hack while better solution from framework js.\n        // Reset the config, especially the ControlPanel which was coming from a parent form view.\n        // It also reset the view switchers which was necessary to make them disappear.\n        useSubEnv({\n            config: {...this.env.methods},\n        });\n    }\n}\n", "/** @odoo-module **/\n\nimport { patch } from \"@web/core/utils/patch\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { ExportDataDialog } from \"@web/views/view_dialogs/export_data_dialog\";\n\npatch(ExportDataDialog.prototype, {\n    async fetchFields(value) {\n        await super.fetchFields(value);\n\n        const analyticLineIdsField = this.knownFields['analytic_line_ids'];\n        if (analyticLineIdsField) {\n            // If analytic_distribution field is here, we remove it to replace it by the new fields\n            this.state.exportList = this.state.exportList.filter(\n                (field) => field.id !== 'analytic_distribution'\n            );\n            const analyticLineFields = await rpc(\"/web/export/get_fields\", {\n                model: analyticLineIdsField.params.model,\n                prefix: analyticLineIdsField.params.prefix,\n                parent_name: analyticLineIdsField.params.parent_field.string,\n                import_compat: analyticLineIdsField.default_export,\n                parent_field_type: analyticLineIdsField.params.parent_field.type,\n                domain:[],\n            });\n            // We exclude auto_account_id as it's a magic field who doesn't need to be exported\n            const filteredAnalyticLineFields = analyticLineFields.filter(\n                (field) => field.params?.model === 'account.analytic.account'\n                    && !field.id.includes('auto_account_id')\n                    || field.id === 'analytic_line_ids/amount'\n            );\n\n            this.state.exportList.push(...filteredAnalyticLineFields);\n        }\n    },\n});\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { Component } from \"@odoo/owl\";\n\nclass MatchingLink extends Component {\n    static props = { ...standardFieldProps };\n    static template = \"account_accountant.MatchingLink\";\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n    }\n\n    async reconcile() {\n        this.action.doAction(\"account_accountant.action_move_line_posted_unreconciled\", {\n            additionalContext: {\n                search_default_partner_id: this.props.record.data.partner_id[0],\n                search_default_account_id: this.props.record.data.account_id[0],\n            },\n        });\n    }\n\n    async viewMatch() {\n        const action = await this.orm.call(\"account.move.line\", \"open_reconcile_view\", [this.props.record.resId], {});\n        this.action.doAction(action, { additionalContext: { is_matched_view: true } });\n    }\n\n    get colorCode() {\n        const matchValue = this.props.record.data[this.props.name];\n        const matchColorValue = matchValue.replace('P', '');\n        if (matchColorValue === '*') {\n            // reserve color code 0 for multi partial matches\n            return 0;\n        } else {\n            // there is 12 available color palette for 'o_tag_color_*'\n            // since the color code 0 has been reserved by 'P*', we can only use color codes between 1 and 11\n            return parseInt(matchColorValue) % 11 + 1;\n        }\n    }\n}\n\nregistry.category(\"fields\").add(\"matching_link_widget\", {\n    component: MatchingLink,\n});\n", "import { AttachmentView } from \"@mail/core/common/attachment_view\";\nimport { onMounted } from \"@odoo/owl\";\nimport { useBus } from \"@web/core/utils/hooks\";\n\nexport class AttachmentViewMoveLine extends AttachmentView {\n    static props = [...AttachmentView.props, \"openInPopout\"];\n    static components = { AttachmentView };\n\n    setup() {\n        super.setup();\n        if (this.props.openInPopout) {\n            onMounted(this.onClickPopout);\n        }\n        // In the mail_popout_service.js the function pollClosedWindow will trigger a resize event on the main window\n        // when the external window is closed. It allow us to know when to hide the preview for small screens.\n        useBus(this.uiService.bus, \"resize\", () => this.env.setPopout(false));\n    }\n}\n", "import { AttachmentViewMoveLine } from \"./attachment_view_move_line\";\n\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { listView } from \"@web/views/list/list_view\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { ListController } from \"@web/views/list/list_controller\";\nimport { SIZES } from \"@web/core/ui/ui_service\";\nimport { useChildSubEnv, useState } from \"@odoo/owl\";\nimport { makeActiveField } from \"@web/model/relational_model/utils\";\n\nexport class AccountMoveLineListController extends ListController {\n    static template = \"account_accountant.MoveLineListView\";\n    static components = {\n        ...ListController.components,\n        AttachmentViewMoveLine,\n    };\n    setup() {\n        super.setup();\n        /** @type {import(\"@mail/core/common/store_service\").Store} */\n        this.store = useState(useService(\"mail.store\"));\n        this.ui = useState(useService(\"ui\"));\n        this.mailPopoutService = useState(useService(\"mail.popout\"));\n        this.attachmentPreviewState = useState({\n            displayAttachment:\n                localStorage.getItem(\"account.move_line_pdf_previewer_hidden\") !== \"false\",\n            selectedRecord: false,\n            thread: null,\n        });\n        this.popout = useState({ active: false });\n\n        useChildSubEnv({\n            setPopout: this.setPopout.bind(this),\n        });\n\n        // We need to override the openRecord from ListController since it does things we do not want like opening the default form view\n        this.openRecord = () => {}\n    }\n\n    get previewEnabled() {\n        return (\n            !this.env.searchModel.context.disable_preview &&\n            (this.ui.size >= SIZES.XXL || this.mailPopoutService.externalWindow)\n        );\n    }\n\n    get modelParams() {\n        const params = super.modelParams;\n        params.config.activeFields.move_attachment_ids = makeActiveField();\n        params.config.activeFields.move_attachment_ids.related = {\n            fields: {\n                mimetype: { name: \"mimetype\", type: \"char\" },\n            },\n            activeFields: {\n                mimetype: makeActiveField(),\n            },\n        };\n        return params;\n    }\n\n    togglePreview() {\n        this.attachmentPreviewState.displayAttachment =\n            !this.attachmentPreviewState.displayAttachment;\n        localStorage.setItem(\n            \"account.move_line_pdf_previewer_hidden\",\n            this.attachmentPreviewState.displayAttachment\n        );\n    }\n\n    setPopout(value) {\n        /**\n         * This function will set the popout value to false or true depending on the situation.\n         * We set popout to True when clicking on a line that has an attachment and then clicking on the popout button.\n         * Once the external page is closed, the popout is set to false again.\n         */\n        if (this.attachmentPreviewState.thread?.attachmentsInWebClientView.length) {\n            this.popout.active = value;\n        }\n    }\n\n    setSelectedRecord(accountMoveLineData) {\n        this.attachmentPreviewState.selectedRecord = accountMoveLineData;\n        this.setThread(this.attachmentPreviewState.selectedRecord);\n    }\n\n    async setThread(accountMoveLineData) {\n        if (!accountMoveLineData || !accountMoveLineData.data.move_attachment_ids.records.length) {\n            this.attachmentPreviewState.thread = null;\n            return;\n        }\n        const thread = this.store.Thread.insert({\n            attachments: accountMoveLineData.data.move_attachment_ids.records.map((attachment) => ({\n                id: attachment.resId,\n                mimetype: attachment.data.mimetype,\n            })),\n            id: accountMoveLineData.data.move_id[0],\n            model: accountMoveLineData.fields[\"move_id\"].relation,\n        });\n        if (!thread.mainAttachment && thread.attachmentsInWebClientView.length > 0) {\n            thread.update({ mainAttachment: thread.attachmentsInWebClientView[0] });\n        }\n        this.attachmentPreviewState.thread = thread;\n    }\n}\n\nexport class AccountMoveLineListRenderer extends ListRenderer {\n    static props = [...ListRenderer.props, \"setSelectedRecord?\"];\n    onCellClicked(record, column, ev) {\n        this.props.setSelectedRecord(record);\n        super.onCellClicked(record, column, ev);\n    }\n\n    findFocusFutureCell(cell, cellIsInGroupRow, direction) {\n        const futureCell = super.findFocusFutureCell(cell, cellIsInGroupRow, direction);\n        if (futureCell) {\n            const dataPointId = futureCell.closest(\"tr\").dataset.id;\n            const record = this.props.list.records.filter((x) => x.id === dataPointId)[0];\n            this.props.setSelectedRecord(record);\n        }\n        return futureCell;\n    }\n}\nexport const AccountMoveLineListView = {\n    ...listView,\n    Renderer: AccountMoveLineListRenderer,\n    Controller: AccountMoveLineListController,\n};\n\nregistry.category(\"views\").add(\"account_move_line_list\", AccountMoveLineListView);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { useSubEnv } from \"@odoo/owl\";\nimport { AccountMoveLineListController, AccountMoveLineListRenderer, AccountMoveLineListView } from \"../move_line_list/move_line_list\";\n\n\nexport class AccountMoveLineReconcileListController extends AccountMoveLineListController {\n\n    setup() {\n        super.setup();\n        useSubEnv({\n            callAutoReconcileAction: this.openAutoReconcileWizard.bind(this),\n        });\n    }\n\n    openAutoReconcileWizard(group=null) {\n        if (group) {\n            return this.actionService.doAction(\"account_accountant.action_open_auto_reconcile_wizard\", {\n                additionalContext: {\n                    domain: group.list.domain,\n                }\n            });\n        } else {\n            return this.actionService.doAction(\"account_accountant.action_open_auto_reconcile_wizard\");\n        }\n    }\n}\n\nexport class AccountMoveLineReconcileListRenderer extends AccountMoveLineListRenderer {\n\n    static groupRowTemplate = \"account_accountant.AccountMoveLineReconcileGroupRow\";\n\n    setup() {\n        super.setup();\n        this.props.list.groups?.map(group => this.toggleGroup(group));  // unfold the first groups (account_id)\n    }\n\n}\n\nexport const AccountMoveLineReconcileLineListView = {\n    ...AccountMoveLineListView,\n    Controller: AccountMoveLineReconcileListController,\n    Renderer: AccountMoveLineReconcileListRenderer,\n    buttonTemplate: \"account_accountant.ListViewReconcile.Buttons\",\n}\n\nregistry.category(\"views\").add(\"account_move_line_reconcile_list\", AccountMoveLineReconcileLineListView);\n", "/** @odoo-module **/\n\nimport { patch } from \"@web/core/utils/patch\";\nimport { accountTourSteps } from \"@account/js/tours/account\";\nimport { stepUtils } from \"@web_tour/tour_service/tour_utils\";\n\n\npatch(accountTourSteps, {\n    goToAccountMenu(description=\"Open Accounting Menu\") {\n        return stepUtils.goToAppSteps('accountant.menu_accounting', description);\n    },\n});\n", "import { monetaryField, MonetaryField } from \"@web/views/fields/monetary/monetary_field\";\nimport { registry } from \"@web/core/registry\";\nimport { floatIsZero } from \"@web/core/utils/numbers\";\n\nexport class MonetaryFieldNoZero extends MonetaryField {\n    static props = {\n        ...MonetaryField.props,\n    };\n\n    /** Override **/\n    get value() {\n        const originalValue = super.value;\n        const decimals = this.currencyDigits ? this.currencyDigits[1] : 2;\n        return floatIsZero(originalValue, decimals) ? false : originalValue;\n    }\n}\n\nexport const monetaryFieldNoZero = {\n    ...monetaryField,\n    component: MonetaryFieldNoZero,\n};\n\nregistry.category(\"fields\").add(\"monetary_no_zero\", monetaryFieldNoZero);\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { Component } from \"@odoo/owl\";\n\nclass OpenMatchLineWidget extends Component {\n    static template = \"purchase.OpenMatchLineWidget\";\n    static props = { ...standardFieldProps };\n\n    setup() {\n        super.setup();\n        this.action = useService(\"action\");\n    }\n\n    async openMatchLine() {\n        this.action.doActionButton({\n            type: \"object\",\n            resId: this.props.record.resId,\n            name: \"action_open_line\",\n            resModel: \"purchase.bill.line.match\",\n        });\n    }\n}\n\nregistry.category(\"fields\").add(\"open_match_line_widget\", {\n    component: OpenMatchLineWidget,\n});\n", "/** @odoo-module */\nimport { ProductCatalogKanbanRecord } from \"@product/product_catalog/kanban_record\";\nimport { ProductCatalogPurchaseOrderLine } from \"./purchase_order_line/purchase_order_line\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useSubEnv } from \"@odoo/owl\";\n\npatch(ProductCatalogKanbanRecord.prototype, {\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        useSubEnv({\n            updatePackagingQuantity: this.updatePackagingQuantity.bind(this),\n        });\n    },\n\n    get orderLineComponent() {\n        if (this.env.orderResModel === \"purchase.order\") {\n            return ProductCatalogPurchaseOrderLine;\n        }\n        return super.orderLineComponent;\n    },\n\n    addProduct() {\n        if (this.productCatalogData.quantity === 0 && this.productCatalogData.min_qty) {\n            super.addProduct(this.productCatalogData.min_qty);\n        } else {\n            super.addProduct(...arguments);\n        }\n    },\n\n    async updatePackagingQuantity(packaging) {\n        const productPackagingQty =\n            Math.floor(this.productCatalogData.quantity / packaging.qty) + 1;\n        this.productCatalogData.quantity = productPackagingQty * packaging.qty;\n        const price = await rpc(\"/product/catalog/update_order_line_info\", {\n            order_id: this.env.orderId,\n            product_id: this.env.productId,\n            product_packaging_id: packaging.id,\n            product_packaging_qty: productPackagingQty,\n            res_model: this.env.orderResModel,\n        });\n        this.productCatalogData.price = parseFloat(price);\n    },\n});\n", "/** @odoo-module */\nimport { ProductCatalogOrderLine } from \"@product/product_catalog/order_line/order_line\";\n\nexport class ProductCatalogPurchaseOrderLine extends ProductCatalogOrderLine {\n    static template = \"ProductCatalogPurchaseOrderLine\";\n    static props = {\n        ...ProductCatalogPurchaseOrderLine.props,\n        min_qty: { type: Number, optional: true },\n        packaging: { type: Object, optional: true },\n        purchase_uom: { type: Object, optional: true },\n        uom: Object,\n    };\n\n    get highlightUoM() {\n        return true;\n    }\n\n    addPackagingQty() {\n        this.env.updatePackagingQuantity(this.props.packaging);\n    }\n}\n", "/** @odoo-module */\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component } from \"@odoo/owl\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\n\nclass ButtonWithNotification extends Component {\n    static template = \"purchase.ButtonWithNotification\";\n    static props = {\n        ...standardWidgetProps,\n        method: String,\n        title: String,\n    };\n    setup() {\n        this.orm = useService(\"orm\");\n        this.notification = useService(\"notification\");\n    }\n\n    async onClick() {\n        const result = await this.orm.call(this.props.record.resModel, this.props.method, [\n            this.props.record.resId,\n        ]);\n        const message = result.toast_message;\n        this.notification.add(message, { type: \"success\" });\n    }\n}\n\nexport const buttonWithNotification = {\n    component: ButtonWithNotification,\n    extractProps: ({ attrs }) => {\n        return {\n            method: attrs.button_name,\n            title: attrs.title,\n        };\n    },\n};\nregistry.category(\"view_widgets\").add(\"toaster_button\", buttonWithNotification);\n", "/** @odoo-module */\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, onWillStart } from \"@odoo/owl\";\n\nexport class PurchaseDashBoard extends Component {\n    static template = \"purchase.PurchaseDashboard\";\n    static props = {};\n    setup() {\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n\n        onWillStart(async () => {\n            this.purchaseData = await this.orm.call(\"purchase.order\", \"retrieve_dashboard\");\n        });\n    }\n\n    /**\n     * This method clears the current search query and activates\n     * the filters found in `filter_name` attibute from button pressed\n     */\n    setSearchContext(ev) {\n        const filter_name = ev.currentTarget.getAttribute(\"filter_name\");\n        const filters = filter_name.split(\",\");\n        const searchItems = this.env.searchModel.getSearchItems((item) =>\n            filters.includes(item.name)\n        );\n        this.env.searchModel.query = [];\n        for (const item of searchItems) {\n            this.env.searchModel.toggleSearchItem(item.id);\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { KanbanRenderer } from \"@web/views/kanban/kanban_renderer\";\nimport { PurchaseDashBoard } from \"@purchase/views/purchase_dashboard\";\n\nexport class PurchaseDashBoardKanbanRenderer extends KanbanRenderer {\n    static template = \"purchase.PurchaseKanbanView\";\n    static components = Object.assign({}, KanbanRenderer.components, { PurchaseDashBoard });\n}\n\nexport const PurchaseDashBoardKanbanView = {\n    ...kanbanView,\n    Renderer: PurchaseDashBoardKanbanRenderer,\n};\n\nregistry.category(\"views\").add(\"purchase_dashboard_kanban\", PurchaseDashBoardKanbanView);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { listView } from \"@web/views/list/list_view\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { PurchaseDashBoard } from \"@purchase/views/purchase_dashboard\";\n\nexport class PurchaseDashBoardRenderer extends ListRenderer {\n    static template = \"purchase.PurchaseListView\";\n    static components = Object.assign({}, ListRenderer.components, { PurchaseDashBoard });\n}\n\nexport const PurchaseDashBoardListView = {\n    ...listView,\n    Renderer: PurchaseDashBoardRenderer,\n};\n\nregistry.category(\"views\").add(\"purchase_dashboard_list\", PurchaseDashBoardListView);\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 PurchaseAdditionalTourSteps from \"@purchase/js/tours/purchase_steps\";\n\nregistry.category(\"web_tour.tours\").add(\"purchase_tour\", {\n    url: \"/odoo\",\n    steps: () => [\n        stepUtils.showAppsMenuItem(),\n        {\n            isActive: [\"community\"],\n            trigger: '.o_app[data-menu-xmlid=\"purchase.menu_purchase_root\"]',\n            content: _t(\n                \"Let's try the Purchase app to manage the flow from purchase to reception and invoice control.\"\n            ),\n            tooltipPosition: \"right\",\n            run: \"click\",\n        },\n        {\n            isActive: [\"enterprise\"],\n            trigger: '.o_app[data-menu-xmlid=\"purchase.menu_purchase_root\"]',\n            content: _t(\n                \"Let's try the Purchase app to manage the flow from purchase to reception and invoice control.\"\n            ),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_purchase_order\",\n        },\n        {\n            trigger: \".o_list_button_add\",\n            content: _t(\"Let's create your first request for quotation.\"),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_purchase_order\",\n        },\n        {\n            trigger: \".o_field_res_partner_many2one[name='partner_id'] input\",\n            content: _t(\"Search a vendor name, or create one on the fly.\"),\n            tooltipPosition: \"bottom\",\n            async run(actions) {\n                const input = this.anchor.querySelector(\"input\");\n                await actions.edit(\"Azure Interior\", input || this.anchor);\n            },\n        },\n        {\n            isActive: [\"auto\"],\n            trigger: \".ui-menu-item > a:contains('Azure Interior')\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_field_res_partner_many2one[name='partner_id'] .o_external_button\",\n        },\n        {\n            trigger: \".o_field_x2many_list_row_add > a\",\n            content: _t(\"Add some products or services to your quotation.\"),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_purchase_order\",\n        },\n        {\n            trigger: \".o_field_widget[name=product_id], .o_field_widget[name=product_template_id]\",\n            content: _t(\"Select a product, or create a new one on the fly.\"),\n            tooltipPosition: \"right\",\n            async run(actions) {\n                const input = this.anchor.querySelector(\"input\");\n                await actions.edit(\"DESK0001\", input || this.anchor);\n            },\n        },\n        {\n            isActive: [\"auto\"],\n            trigger: \"a:contains('DESK0001')\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_field_text[name='name'] textarea:value(DESK0001)\",\n        },\n        {\n            trigger: \".o_purchase_order\",\n        },\n        {\n            trigger: \"div.o_field_widget[name='product_qty'] input \",\n            content: _t(\"Indicate the product quantity you want to order.\"),\n            tooltipPosition: \"right\",\n            run: \"edit 12.0\",\n        },\n        {\n            isActive: [\"auto\", \"mobile\"],\n            trigger: \".o_statusbar_buttons .o_arrow_button_current[name='action_rfq_send']\",\n        },\n        ...stepUtils.statusbarButtonsSteps(\n            \"Send by Email\",\n            _t(\"Send the request for quotation to your vendor.\")\n        ),\n        {\n            trigger: \".modal-footer button[name='action_send_mail']\",\n        },\n        {\n            trigger: \".modal-footer button[name='action_send_mail']\",\n            content: _t(\"Send the request for quotation to your vendor.\"),\n            tooltipPosition: \"left\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_purchase_order\",\n        },\n        {\n            content: \"Select price\",\n            trigger: 'tbody tr.o_data_row .o_list_number[name=\"price_unit\"]',\n        },\n        {\n            trigger: \"tbody tr.o_data_row .o_list_number[name='price_unit']\",\n            content: _t(\n                \"Once you get the price from the vendor, you can complete the purchase order with the right price.\"\n            ),\n            tooltipPosition: \"right\",\n            run: \"edit 200.00\",\n        },\n        {\n            isActive: [\"auto\"],\n            trigger: \".o_purchase_order\",\n            run: \"click\",\n        },\n        ...stepUtils.statusbarButtonsSteps(\"Confirm Order\", _t(\"Confirm your purchase.\")),\n        ...new PurchaseAdditionalTourSteps()._get_purchase_stock_steps(),\n    ],\n});\n", "/** @odoo-module **/\n\nclass PurchaseAdditionalTourSteps {\n    _get_purchase_stock_steps() {\n        return [\n            {\n                // Useless final step to trigger congratulation message\n                isActive: [\"auto\"],\n                trigger: \".o_purchase_order\",\n                run: \"click\",\n            },\n        ];\n    }\n}\n\nexport default PurchaseAdditionalTourSteps;\n", "/** @odoo-modules */\n\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { Component } from \"@odoo/owl\";\n\nclass IAPActionButtonsWidget extends Component {\n    static template = \"iap.ActionButtonsWidget\";\n    static props = {\n        ...standardWidgetProps,\n        serviceName: String,\n        showServiceButtons: Boolean,\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n    }\n\n    async onViewServicesClicked() {\n        this.action.doAction(\"iap.iap_account_action\");\n    }\n\n    async onManageServiceLinkClicked() {\n        const account_id = await this.orm.silent.call(\"iap.account\", \"get_account_id\", [this.props.serviceName]);\n        this.action.doAction({\n            type: \"ir.actions.act_window\",\n            res_model: \"iap.account\",\n            res_id: account_id,\n            views: [[false, \"form\"]],\n        });\n    }\n}\n\nexport const iapActionButtonsWidget = {\n    component: IAPActionButtonsWidget,\n    extractProps: ({ attrs }) => {\n        return {\n            serviceName: attrs.service_name,\n            showServiceButtons: !Boolean(attrs.hide_service),\n        };\n    },\n};\nregistry.category(\"view_widgets\").add(\"iap_buy_more_credits\", iapActionButtonsWidget);\n", "/** @odoo-module */\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Component, onWillStart } from \"@odoo/owl\";\n\nclass InsufficientCreditDialog extends Component {\n    static components = { Dialog };\n    static template = \"iap.InsufficientCreditDialog\";\n    static props = {\n        errorData: Object,\n        close: Function,\n    };\n    setup() {\n        this.orm = useService(\"orm\");\n        onWillStart(this.onWillStart);\n    }\n\n    async onWillStart() {\n        const { errorData } = this.props;\n        this.url = await this.orm.call(\"iap.account\", \"get_credits_url\", [], {\n            base_url: errorData.base_url,\n            service_name: errorData.service_name,\n            credit: errorData.credit,\n            trial: errorData.trial,\n        });\n        this.style = errorData.body ? \"padding:0;\" : \"\";\n        const { isEnterprise } = odoo.info;\n        if (errorData.trial && isEnterprise) {\n            this.buttonMessage = _t(\"Start a Trial at Odoo\");\n        } else {\n            this.buttonMessage = _t(\"Buy credits\");\n        }\n    }\n\n    buyCredits() {\n        window.open(this.url, \"_blank\");\n        this.props.close();\n    }\n}\n\nfunction insufficientCreditHandler(env, error, originalError) {\n    if (!originalError) {\n        return false;\n    }\n    const { data } = originalError;\n    if (data && data.name === \"odoo.addons.iap.tools.iap_tools.InsufficientCreditError\") {\n        env.services.dialog.add(InsufficientCreditDialog, {\n            errorData: JSON.parse(data.message),\n        });\n        return true;\n    }\n    return false;\n}\n\nregistry\n    .category(\"error_handlers\")\n    .add(\"insufficientCreditHandler\", insufficientCreditHandler, { sequence: 0 });\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nimport { markup } from \"@odoo/owl\";\n\nexport const iapNotificationService = {\n    dependencies: [\"bus_service\", \"notification\"],\n\n    start(env, { bus_service, notification }) {\n        bus_service.subscribe(\"iap_notification\", (params) => {\n            if (params.type == \"no_credit\") {\n                displayCreditErrorNotification(params);\n            } else {\n                displayNotification(params);\n            }\n        });\n        bus_service.start();\n\n        function displayNotification(params) {\n            notification.add(params.message, {\n                title: params.title,\n                type: params.type,\n            });\n        }\n\n        function displayCreditErrorNotification(params) {\n            // \u2139\ufe0f `_t` can only be inlined directly inside JS template literals\n            // after Babel has been updated to version 2.12.\n            const translatedText = _t(\"Buy more credits\");\n            const message = markup(`\n            <a class='btn btn-link' href='${params.get_credits_url}' target='_blank'>\n                <i class='oi oi-arrow-right'></i>\n                ${translatedText}\n            </a>`);\n            notification.add(message, {\n                title: params.title,\n                type: 'danger',\n            });\n        }\n    }\n};\n\nregistry.category(\"services\").add(\"iapNotification\", iapNotificationService);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { PhoneField, phoneField, formPhoneField } from \"@web/views/fields/phone/phone_field\";\nimport { SendSMSButton } from '@sms/components/sms_button/sms_button';\n\npatch(PhoneField, {\n    components: {\n        ...PhoneField.components,\n        SendSMSButton\n    },\n    defaultProps: {\n        ...PhoneField.defaultProps,\n        enableButton: true,\n    },\n    props: {\n        ...PhoneField.props,\n        enableButton: { type: Boolean, optional: true },\n    },\n});\n\nconst patchDescr = () => ({\n    extractProps({ options }) {\n        const props = super.extractProps(...arguments);\n        props.enableButton = options.enable_sms;\n        return props;\n    },\n    supportedOptions: [{\n        label: _t(\"Enable SMS\"),\n        name: \"enable_sms\",\n        type: \"boolean\",\n        default: true,\n    }],\n});\n\npatch(phoneField, patchDescr());\npatch(formPhoneField, patchDescr());\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, status } from \"@odoo/owl\";\n\nexport class SendSMSButton extends Component {\n    static template = \"sms.SendSMSButton\";\n    static props = [\"*\"];\n    setup() {\n        this.action = useService(\"action\");\n        this.title = _t(\"Send SMS\");\n    }\n    get phoneHref() {\n        return \"sms:\" + this.props.record.data[this.props.name].replace(/\\s+/g, \"\");\n    }\n    async onClick() {\n        await this.props.record.save();\n        this.action.doAction(\n            {\n                type: \"ir.actions.act_window\",\n                target: \"new\",\n                name: this.title,\n                res_model: \"sms.composer\",\n                views: [[false, \"form\"]],\n                context: {\n                    ...user.context,\n                    default_res_model: this.props.record.resModel,\n                    default_res_id: this.props.record.resId,\n                    default_number_field_name: this.props.name,\n                    default_composition_mode: \"comment\",\n                },\n            },\n            {\n                onClose: () => {\n                    if (status(this) === \"destroyed\") {\n                        return;\n                    }\n                    this.props.record.load();\n                },\n            }\n        );\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport {\n    EmojisTextField,\n    emojisTextField,\n} from \"@mail/views/web/fields/emojis_text_field/emojis_text_field\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\n\n/**\n * SmsWidget is a widget to display a textarea (the body) and a text representing\n * the number of SMS and the number of characters. This text is computed every\n * time the user changes the body.\n */\nexport class SmsWidget extends EmojisTextField {\n    static template = \"sms.SmsWidget\";\n    setup() {\n        super.setup();\n        this._emojiAdded = () => this.props.record.update({ [this.props.name]: this.targetEditElement.el.value });\n        this.notification = useService('notification');\n    }\n\n    get encoding() {\n        return this._extractEncoding(this.props.record.data[this.props.name] || '');\n    }\n    get nbrChar() {\n        const content = this._getValueForSmsCounts(this.props.record.data[this.props.name] || \"\");\n        return content.length + (content.match(/\\n/g) || []).length;\n    }\n    get nbrCharExplanation() {\n        return \"\";\n    }\n    get nbrSMS() {\n        return this._countSMS(this.nbrChar, this.encoding);\n    }\n\n    //--------------------------------------------------------------------------\n    // Private: SMS\n    //--------------------------------------------------------------------------\n\n    /**\n     * Count the number of SMS of the content\n     * @private\n     * @returns {integer} Number of SMS\n     */\n    _countSMS(nbrChar, encoding) {\n        if (nbrChar === 0) {\n            return 0;\n        }\n        if (encoding === 'UNICODE') {\n            if (nbrChar <= 70) {\n                return 1;\n            }\n            return Math.ceil(nbrChar / 67);\n        }\n        if (nbrChar <= 160) {\n            return 1;\n        }\n        return Math.ceil(nbrChar / 153);\n    }\n\n    /**\n     * Extract the encoding depending on the characters in the content\n     * @private\n     * @param {String} content Content of the SMS\n     * @returns {String} Encoding of the content (GSM7 or UNICODE)\n     */\n    _extractEncoding(content) {\n        if (String(content).match(RegExp(\"^[@\u00a3$\u00a5\u00e8\u00e9\u00f9\u00ec\u00f2\u00c7\\\\n\u00d8\u00f8\\\\r\u00c5\u00e5\u0394_\u03a6\u0393\u039b\u03a9\u03a0\u03a8\u03a3\u0398\u039e\u00c6\u00e6\u00df\u00c9 !\\\\\\\"#\u00a4%&'()*+,-./0123456789:;<=>?\u00a1ABCDEFGHIJKLMNOPQRSTUVWXYZ\u00c4\u00d6\u00d1\u00dc\u00a7\u00bfabcdefghijklmnopqrstuvwxyz\u00e4\u00f6\u00f1\u00fc\u00e0]*$\"))) {\n            return 'GSM7';\n        }\n        return 'UNICODE';\n    }\n\n    /**\n     * Implement if more characters are going to be sent then those appearing in\n     * value, if that value is processed before being sent.\n     * E.g., links are converted to trackers in mass_mailing_sms.\n     *\n     * Note: goes with an explanation in nbrCharExplanation\n     *\n     * @param {String} value content to be parsed for counting extra characters\n     * @return string length-corrected value placeholder for the post-processed\n     * state\n     */\n    _getValueForSmsCounts(value) {\n        return value;\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     * @private\n     */\n    async onBlur() {\n        await super.onBlur();\n        var content = this.props.record.data[this.props.name] || '';\n        if( !content.trim().length && content.length > 0) {\n            this.notification.add(\n                _t(\"Your SMS Text Message must include at least one non-whitespace character\"),\n                { type: 'danger' },\n            )\n            await this.props.record.update({ [this.props.name]: content.trim() });\n        }\n    }\n\n    /**\n     * @override\n     * @private\n     */\n    async onInput(ev) {\n        super.onInput(...arguments);\n        await this.props.record.update({ [this.props.name]: this.targetEditElement.el.value });\n    }\n}\n\nexport const smsWidget = {\n    ...emojisTextField,\n    component: SmsWidget,\n    additionalClasses: [\n        ...(emojisTextField.additionalClasses || []),\n        \"o_field_text\",\n        \"o_field_text_emojis\",\n    ],\n};\n\nregistry.category(\"fields\").add(\"sms_widget\", smsWidget);\n", "/** @odoo-module */\n\nimport { Failure } from \"@mail/core/common/failure_model\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Failure.prototype, {\n    get iconSrc() {\n        if (this.type === \"sms\") {\n            return \"/sms/static/img/sms_failure.svg\";\n        }\n        return super.iconSrc;\n    },\n    get body() {\n        if (this.type === \"sms\") {\n            if (this.notifications.length === 1 && this.lastMessage?.thread) {\n                return _t(\"An error occurred when sending an SMS on \u201c%(record_name)s\u201d\", {\n                    record_name: this.lastMessage.thread.name,\n                });\n            }\n            return _t(\"An error occurred when sending an SMS\");\n        }\n        return super.body;\n    },\n});\n", "/** @odoo-module */\n\nimport { Notification } from \"@mail/core/common/notification_model\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Notification.prototype, {\n    get icon() {\n        if (this.notification_type === \"sms\") {\n            return \"fa fa-mobile\";\n        }\n        return super.icon;\n    },\n    get label() {\n        if (this.notification_type === \"sms\") {\n            return _t(\"SMS\");\n        }\n        return super.label;\n    },\n});\n", "/** @odoo-module */\n\nimport { MessagingMenu } from \"@mail/core/public_web/messaging_menu\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(MessagingMenu.prototype, {\n    openFailureView(failure) {\n        if (failure.type === \"email\") {\n            return super.openFailureView(failure);\n        }\n        this.env.services.action.doAction({\n            name: _t(\"SMS Failures\"),\n            type: \"ir.actions.act_window\",\n            view_mode: \"kanban,list,form\",\n            views: [\n                [false, \"kanban\"],\n                [false, \"list\"],\n                [false, \"form\"],\n            ],\n            target: \"current\",\n            res_model: failure.resModel,\n            domain: [[\"message_has_sms_error\", \"=\", true]],\n            context: { create: false },\n        });\n        this.dropdown.close();\n    },\n    getFailureNotificationName(failure) {\n        if (failure.type === \"sms\") {\n            return _t(\"SMS Failure: %(modelName)s\", { modelName: failure.modelName });\n        }\n        return super.getFailureNotificationName(...arguments);\n    },\n});\n", "/** @odoo-module */\n\nimport { Message } from \"@mail/core/common/message\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Message.prototype, {\n    onClickFailure() {\n        if (this.message.message_type === \"sms\") {\n            this.env.services.action.doAction(\"sms.sms_resend_action\", {\n                additionalContext: {\n                    default_mail_message_id: this.message.id,\n                },\n            });\n        } else {\n            super.onClickFailure(...arguments);\n        }\n    },\n});\n", "/** @odoo-module **/\n\nimport { Component, useExternalListener, useEffect, useRef } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useThrottleForAnimation } from \"@web/core/utils/timing\";\n\nclass ActionsOne2ManyField extends Component {\n    static props = [\"*\"];\n    static template = \"base_automation.ActionsOne2ManyField\";\n    static actionStates = {\n        code: _t(\"Execute Python Code\"),\n        object_create: _t(\"Create a new Record\"),\n        object_write: _t(\"Update the Record\"),\n        multi: _t(\"Execute several actions\"),\n        mail_post: _t(\"Send email\"),\n        followers: _t(\"Add followers\"),\n        remove_followers: _t(\"Remove followers\"),\n        next_activity: _t(\"Create next activity\"),\n        sms: _t(\"Send SMS\"),\n    };\n    setup() {\n        this.root = useRef(\"root\");\n\n        let adaptCounter = 0;\n        useEffect(\n            () => {\n                this.adapt();\n            },\n            () => [adaptCounter]\n        );\n        const throttledRenderAndAdapt = useThrottleForAnimation(() => {\n            adaptCounter++;\n            this.render();\n        });\n        useExternalListener(window, \"resize\", throttledRenderAndAdapt);\n        this.currentActions = this.props.record.data[this.props.name].records;\n        this.hiddenActionsCount = 0;\n    }\n    async adapt() {\n        // --- Initialize ---\n        // use getBoundingClientRect to get unrounded width\n        // of the elements in order to avoid rounding issues\n        const rootWidth = this.root.el.getBoundingClientRect().width;\n\n        // remove all d-none classes (needed to get the real width of the elements)\n        const actionsEls = Array.from(this.root.el.children).filter((el) => el.dataset.actionId);\n        actionsEls.forEach((el) => el.classList.remove(\"d-none\"));\n        const actionsTotalWidth = actionsEls.reduce(\n            (sum, el) => sum + el.getBoundingClientRect().width,\n            0\n        );\n\n        // --- Check first overflowing action ---\n        let overflowingActionId;\n        if (actionsTotalWidth > rootWidth) {\n            let width = 56; // for the ellipsis\n            for (const el of actionsEls) {\n                const elWidth = el.getBoundingClientRect().width;\n                if (width + elWidth > rootWidth) {\n                    // All the remaining elements are overflowing\n                    overflowingActionId = el.dataset.actionId;\n                    const firstOverflowingEl = actionsEls.find(\n                        (el) => el.dataset.actionId === overflowingActionId\n                    );\n                    const firstOverflowingIndex = actionsEls.indexOf(firstOverflowingEl);\n                    const overflowingEls = actionsEls.slice(firstOverflowingIndex);\n                    // hide overflowing elements\n                    overflowingEls.forEach((el) => el.classList.add(\"d-none\"));\n                    break;\n                }\n                width += elWidth;\n            }\n        }\n\n        // --- Final rendering ---\n        const initialHiddenActionsCount = this.hiddenActionsCount;\n        this.hiddenActionsCount = overflowingActionId\n            ? this.currentActions.length -\n              this.currentActions.findIndex((action) => action.id === overflowingActionId)\n            : 0;\n        if (initialHiddenActionsCount !== this.hiddenActionsCount) {\n            // Render only if hidden actions count has changed.\n            return this.render();\n        }\n    }\n    getActionType(action) {\n        return this.constructor.actionStates[action.data.state] || action.data.state;\n    }\n    get moreText() {\n        const isPlural = this.hiddenActionsCount > 1;\n        return isPlural ? _t(\"%s actions\", this.hiddenActionsCount) : _t(\"1 action\");\n    }\n}\n\nconst actionsOne2ManyField = {\n    component: ActionsOne2ManyField,\n    relatedFields: [\n        { name: \"name\", type: \"char\" },\n        {\n            name: \"state\",\n            type: \"selection\",\n            selection: [\n                [\"code\", _t(\"Execute Python Code\")],\n                [\"object_create\", _t(\"Create a new Record\")],\n                [\"object_write\", _t(\"Update the Record\")],\n                [\"multi\", _t(\"Execute several actions\")],\n                [\"mail_post\", _t(\"Send email\")],\n                [\"followers\", _t(\"Add followers\")],\n                [\"remove_followers\", _t(\"Remove followers\")],\n                [\"next_activity\", _t(\"Create next activity\")],\n                [\"sms\", _t(\"Send SMS\")],\n            ],\n        },\n        // Execute Python Code\n        { name: \"code\", type: \"text\" },\n        // Create\n        { name: \"crud_model_id\", type: \"many2one\" },\n        { name: \"crud_model_name\", type: \"char\" },\n        // Add Followers\n        { name: \"partner_ids\", type: \"many2many\" },\n        // Message Post / Email\n        { name: \"template_id\", type: \"many2one\" },\n        { name: \"mail_post_autofollow\", type: \"boolean\" },\n        {\n            name: \"mail_post_method\",\n            type: \"selection\",\n            selection: [\n                [\"email\", _t(\"Email\")],\n                [\"comment\", _t(\"Post as Message\")],\n                [\"note\", _t(\"Post as Note\")],\n            ],\n        },\n        // Schedule Next Activity\n        { name: \"activity_type_id\", type: \"many2one\" },\n        { name: \"activity_summary\", type: \"char\" },\n        { name: \"activity_note\", type: \"html\" },\n        { name: \"activity_date_deadline_range\", type: \"integer\" },\n        {\n            name: \"activity_date_deadline_range_type\",\n            type: \"selection\",\n            selection: [\n                [\"days\", _t(\"Days\")],\n                [\"weeks\", _t(\"Weeks\")],\n                [\"months\", _t(\"Months\")],\n            ],\n        },\n        {\n            name: \"activity_user_type\",\n            type: \"selection\",\n            selection: [\n                [\"specific\", _t(\"Specific User\")],\n                [\"generic\", _t(\"Generic User\")],\n            ],\n        },\n        { name: \"activity_user_id\", type: \"many2one\" },\n        { name: \"activity_user_field_name\", type: \"char\" },\n    ],\n};\n\nregistry.category(\"fields\").add(\"base_automation_actions_one2many\", actionsOne2ManyField);\n", "/** @odoo-module */\n\nimport { RPCErrorDialog } from \"@web/core/errors/error_dialogs\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { user } from \"@web/core/user\";\n\nexport class BaseAutomationErrorDialog extends RPCErrorDialog {\n    static template = \"base_automation.ErrorDialog\";\n    setup() {\n        super.setup(...arguments);\n        const { id, name } = this.props.data.context.base_automation;\n        this.automationId = id;\n        this.automationName = name;\n        this.isUserAdmin = user.isAdmin;\n        this.actionService = useService(\"action\");\n        this.orm = useService(\"orm\");\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * This method is called when the user clicks on the 'Disable Automation Rule' button\n     * displayed when a crash occurs in the evaluation of an automation rule.\n     * Then, we write `active` to `False` on the automation rule to disable it.\n     *\n     * @private\n     * @param {MouseEvent} ev\n     */\n    async disableAutomation(ev) {\n        await this.orm.write(\"base.automation\", [this.automationId], { active: false });\n        this.props.close();\n    }\n    /**\n     * This method is called when the user clicks on the 'Edit action' button\n     * displayed when a crash occurs in the evaluation of an automation rule.\n     * Then, we redirect the user to the automation rule form.\n     *\n     * @private\n     * @param {MouseEvent} ev\n     */\n    editAutomation(ev) {\n        this.actionService.doAction({\n            name: \"Automation Rules\",\n            res_model: \"base.automation\",\n            res_id: this.automationId,\n            views: [[false, \"form\"]],\n            type: \"ir.actions.act_window\",\n            view_mode: \"form\",\n            target: \"new\",\n        });\n        this.props.close();\n    }\n}\n\nregistry.category(\"error_dialogs\").add(\"base_automation\", BaseAutomationErrorDialog);\n", "/** @odoo-module */\n\nimport { useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\nimport { selectionField, SelectionField } from \"@web/views/fields/selection/selection_field\";\nimport { TRIGGER_FILTERS } from \"./utils\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nconst OPT_GROUPS = [\n    {\n        group: { sequence: 10, key: \"values\", name: _t(\"Values Updated\") },\n        triggers: [\n            \"on_stage_set\",\n            \"on_user_set\",\n            \"on_tag_set\",\n            \"on_state_set\",\n            \"on_priority_set\",\n            \"on_archive\",\n            \"on_unarchive\",\n        ],\n    },\n    {\n        group: { sequence: 30, key: \"timing\", name: _t(\"Timing Conditions\") },\n        triggers: [\"on_time\", \"on_time_created\", \"on_time_updated\"],\n    },\n    {\n        group: { sequence: 40, key: \"custom\", name: _t(\"Custom\") },\n        triggers: [\"on_create_or_write\", \"on_unlink\", \"on_change\"],\n    },\n    {\n        group: { sequence: 50, key: \"external\", name: _t(\"External\") },\n        triggers: [\"on_webhook\"],\n    },\n    {\n        group: { sequence: 20, key: \"mail\", name: _t(\"Email Events\") },\n        triggers: [\"on_message_sent\", \"on_message_received\"],\n    },\n    {\n        group: { sequence: 60, key: \"deprecated\", name: _t(\"Deprecated (do not use)\") },\n        triggers: [\"on_create\", \"on_write\"],\n    },\n];\n\nfunction computeDerivedOptions(options, fields, currentSelection, { excludeGroups = [] } = {}) {\n    // filter options to display, derived from the current value and the model fields\n    const derivedOptions = [];\n    for (const [value, label] of options) {\n        const { group, triggers } = OPT_GROUPS.find((g) => g.triggers.includes(value));\n        if (\n            (group.key === \"deprecated\" && !triggers.includes(currentSelection)) ||\n            excludeGroups.includes(group.key)\n        ) {\n            // skip deprecated triggers if the current value is not deprecated\n            continue;\n        }\n        const filterFn = TRIGGER_FILTERS[value];\n        if (filterFn) {\n            const triggerFields = fields.filter(filterFn);\n            if (triggerFields.length === 0) {\n                // skip triggers that don't have any corresponding field\n                continue;\n            }\n        }\n\n        const option = { group, value, label };\n        derivedOptions.push(option);\n    }\n    return derivedOptions;\n}\n\nexport class TriggerSelectionField extends SelectionField {\n    static template = \"base_automation.TriggerSelectionField\";\n    setup() {\n        super.setup();\n        this.groupedOptions = useState([]);\n\n        const orm = useService(\"orm\");\n        let lastRelatedModelId;\n        let relatedModelFields;\n        useRecordObserver(async (record) => {\n            const { data, fields } = record;\n            const modelId = data.model_id?.[0];\n            if (lastRelatedModelId !== modelId) {\n                lastRelatedModelId = modelId;\n                relatedModelFields = await orm.searchRead(\n                    \"ir.model.fields\",\n                    [[\"model_id\", \"=\", modelId]],\n                    [\"field_description\", \"name\", \"ttype\", \"relation\"]\n                );\n            }\n\n            // first, compute the derived options\n            const derivedOptions = computeDerivedOptions(\n                fields[this.props.name].selection,\n                relatedModelFields,\n                data[this.props.name],\n                { excludeGroups: data.model_is_mail_thread ? [] : [\"mail\"] }\n            );\n\n            // then group and sort them\n            this.groupedOptions.length = 0;\n            for (const option of derivedOptions) {\n                const group = this.groupedOptions.find((g) => g.key === option.group.key) ?? {\n                    ...option.group,\n                    options: [],\n                };\n                group.options.push(option);\n                if (!this.groupedOptions.includes(group)) {\n                    this.groupedOptions.push(group);\n                }\n            }\n            this.groupedOptions.sort((a, b) => a.sequence - b.sequence);\n        });\n    }\n}\n\nexport const triggerSelectionField = {\n    ...selectionField,\n    component: TriggerSelectionField,\n    fieldDependencies: [{ name: \"model_is_mail_thread\", type: \"boolean\" }],\n};\nregistry.category(\"fields\").add(\"base_automation_trigger_selection\", triggerSelectionField);\n", "/* @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { KanbanHeader } from \"@web/views/kanban/kanban_header\";\nimport { TRIGGER_FILTERS } from \"./utils\";\n\nconst SUPPORTED_TRIGGERS = [\n    \"on_stage_set\",\n    \"on_tag_set\",\n    \"on_state_set\",\n    \"on_priority_set\",\n    \"on_user_set\",\n    \"on_archive\",\n];\n\nfunction enrichContext(context, group) {\n    const { displayName, groupByField, value } = group;\n    const { name, relation, type: ttype } = groupByField;\n    for (const trigger of SUPPORTED_TRIGGERS) {\n        if (!TRIGGER_FILTERS[trigger]({ name, relation, ttype })) {\n            continue;\n        }\n        switch (trigger) {\n            case \"on_stage_set\":\n                return {\n                    ...context,\n                    default_trigger: trigger,\n                    default_name: _t('Stage is set to \"%s\"', displayName),\n                    default_trg_field_ref: value,\n                };\n            case \"on_tag_set\":\n                return {\n                    ...context,\n                    default_trigger: trigger,\n                    default_name: _t('\"%s\" tag is added', displayName),\n                    default_trg_field_ref: value,\n                };\n            default:\n                return { ...context, default_trigger: trigger };\n        }\n    }\n\n    // Default trigger\n    return { ...context, default_trigger: \"on_create_or_write\" };\n}\n\npatch(KanbanHeader.prototype, {\n    setup() {\n        super.setup();\n        this.action = useService(\"action\");\n    },\n\n    /**\n     * @override\n     */\n    get permissions() {\n        const permissions = super.permissions;\n        Object.defineProperty(permissions, \"canEditAutomations\", {\n            get: () => user.isAdmin,\n            configurable: true,\n        });\n        return permissions;\n    },\n\n    async openAutomations() {\n        return this._openAutomations();\n    },\n\n    async _openAutomations() {\n        const domain = [[\"model\", \"=\", this.props.list.resModel]];\n        const modelId = await this.orm.search(\"ir.model\", domain, { limit: 1 });\n        const context = {\n            active_test: false,\n            default_model_id: modelId[0],\n            search_default_model_id: modelId[0],\n        };\n        this.action.doAction(\"base_automation.base_automation_act\", {\n            additionalContext: enrichContext(context, this.group),\n        });\n    },\n});\n\nregistry.category(\"kanban_header_config_items\").add(\n    \"open_automations\",\n    {\n        label: _t(\"Automations\"),\n        method: \"openAutomations\",\n        isVisible: ({ permissions }) => permissions.canEditAutomations,\n        class: \"o_column_automations\",\n    },\n    { sequence: 25, force: true }\n);\n", "/** @odoo-module */\n\nexport const TRIGGER_FILTERS = {\n    on_create_or_write: (f) => true,\n    on_create: (f) => true,\n    on_write: (f) => true,\n    on_change: (f) => true,\n    on_unlink: (f) => true,\n    on_time: (f) => true,\n    on_time_created: (f) => f.ttype === \"datetime\" && f.name === \"create_date\",\n    on_time_updated: (f) => f.ttype === \"datetime\" && f.name === \"write_date\",\n    on_stage_set: (f) =>\n        f.ttype === \"many2one\" && [\"stage_id\", \"x_studio_stage_id\"].includes(f.name),\n    on_user_set: (f) =>\n        f.relation === \"res.users\" &&\n        [\"many2one\", \"many2many\"].includes(f.ttype) &&\n        [\"user_id\", \"user_ids\", \"x_studio_user_id\", \"x_studio_user_ids\"].includes(f.name),\n    on_tag_set: (f) => f.ttype === \"many2many\" && [\"tag_ids\", \"x_studio_tag_ids\"].includes(f.name),\n    on_state_set: (f) => f.ttype === \"selection\" && [\"state\", \"x_studio_state\"].includes(f.name),\n    on_priority_set: (f) =>\n        f.ttype === \"selection\" && [\"priority\", \"x_studio_priority\"].includes(f.name),\n    on_archive: (f) => f.ttype === \"boolean\" && [\"active\", \"x_active\"].includes(f.name),\n    on_unarchive: (f) => f.ttype === \"boolean\" && [\"active\", \"x_active\"].includes(f.name),\n    on_webhook: (f) => true,\n};\n", "/** @odoo-module */\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\n\nexport class ImportModuleListRenderer extends ListRenderer {\n\n    get hasSelectors() {\n        return super.hasSelectors && this.props.list.records.every(record => record._values.module_type != 'industries');\n    }\n\n    async onCellClicked(record, column, ev) {\n        if (record._values.module_type && record._values.module_type !== 'official') {\n            const re_action = {\n                name: \"more_info\",\n                res_model: \"ir.module.module\",\n                res_id: -1,\n                type: \"ir.actions.act_window\",\n                views: [[false, \"form\"]],\n                context: {\n                    'module_name': record._values.name,\n                    'module_type': record._values.module_type,\n                }\n            }\n            this.env.services.action.doAction(re_action);\n        }\n        else{\n            super.onCellClicked(record, column, ev);\n        }\n    }\n}\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { listView } from \"@web/views/list/list_view\";\nimport { ImportModuleListRenderer } from \"./base_import_list_renderer\";\n\n\nexport const ImportModuleListView = {\n    ...listView,\n    Renderer: ImportModuleListRenderer,\n}\n\nregistry.category(\"views\").add(\"ir_module_module_tree_view\", ImportModuleListView);\n", "/** @odoo-module **/\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { Component, useRef } from \"@odoo/owl\";\n\nclass StudioSystray extends Component {\n    static template = \"web_studio.SystrayItem\";\n    static props = {};\n    setup() {\n        this.hm = useService(\"home_menu\");\n        this.studio = useService(\"studio\");\n        this.rootRef = useRef(\"root\");\n        this.isLoading = false;\n        this.env.bus.addEventListener(\"ACTION_MANAGER:UPDATE\", () => {\n            this.isLoading = true;\n            if (this.rootRef.el) {\n                this.rootRef.el.classList.toggle(\"o_disabled\", this.buttonDisabled);\n            }\n        });\n        this.env.bus.addEventListener(\"ACTION_MANAGER:UI-UPDATED\", (ev) => {\n            this.isLoading = false;\n            const mode = ev.detail;\n            if (mode !== \"new\" && this.rootRef.el) {\n                this.rootRef.el.classList.toggle(\"o_disabled\", this.buttonDisabled);\n            }\n        });\n    }\n    get buttonDisabled() {\n        return this.isLoading || !this.studio.isStudioEditable();\n    }\n    _onClick() {\n        if (!this.isLoading) {\n            this.studio.open();\n        }\n    }\n}\n\nexport const systrayItem = {\n    Component: StudioSystray,\n    isDisplayed: () => user.isSystem,\n};\n\nregistry.category(\"systray\").add(\"StudioSystrayItem\", systrayItem, { sequence: 1 });\n", "/** @odoo-module **/\n\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { resetViewCompilerCache } from \"@web/views/view_compiler\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport { EventBus, onWillUnmount, useState } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { router } from \"@web/core/browser/router\";\n\nconst URL_VIEW_KEY = \"_view_type\";\nconst URL_ACTION_KEY = \"_action\";\nconst URL_TAB_KEY = \"_tab\";\nconst URL_MODE_KEY = \"mode\";\nconst URL_REPORT_ID_KEY = \"_report_id\";\n\nexport const MODES = {\n    EDITOR: \"editor\",\n    HOME_MENU: \"home_menu\",\n    APP_CREATOR: \"app_creator\",\n};\n\nexport class NotEditableActionError extends Error {}\n\nconst SUPPORTED_VIEW_TYPES = {\n    activity: _t(\"Activity\"),\n    calendar: _t(\"Calendar\"),\n    cohort: _t(\"Cohort\"),\n    form: _t(\"Form\"),\n    gantt: _t(\"Gantt\"),\n    graph: _t(\"Graph\"),\n    kanban: _t(\"Kanban\"),\n    list: _t(\"List\"),\n    map: _t(\"Map\"),\n    pivot: _t(\"Pivot\"),\n    search: _t(\"Search\"),\n};\n\nexport function viewTypeToString(vType) {\n    return SUPPORTED_VIEW_TYPES[vType] || vType;\n}\n\nexport const studioService = {\n    dependencies: [\"action\", \"home_menu\", \"menu\", \"notification\"],\n    async start(env, { menu, notification }) {\n        const supportedViewTypes = Object.keys(SUPPORTED_VIEW_TYPES);\n\n        function _getCurrentAction() {\n            const currentController = env.services.action.currentController;\n            return currentController && !currentController.virtual ? currentController.action : null;\n        }\n\n        function _isStudioEditable(action) {\n            if (action.type === \"ir.actions.client\") {\n                // home_menu is somehow customizable (app creator)\n                return action.tag === \"menu\" ? true : false;\n            }\n            if (action.type === \"ir.actions.act_window\" && action.xml_id) {\n                if (\n                    action.res_model.indexOf(\"settings\") > -1 &&\n                    action.res_model.indexOf(\"x_\") !== 0\n                ) {\n                    return false; // settings views aren't editable; but x_settings is\n                }\n                if (action.res_model === \"board.board\") {\n                    return false; // dashboard isn't editable\n                }\n                if (action.view_mode === \"qweb\") {\n                    // Apparently there is a QWebView that allows to\n                    // implement ActWindow actions that are completely custom\n                    // but not editable by studio\n                    return false;\n                }\n                if (action.res_model === \"knowledge.article\") {\n                    // The knowledge form view is very specific and custom, it doesn't make sense\n                    // to edit it. Editing the list and kanban is more debatable, but for simplicity's sake\n                    // we set them to not editable too.\n                    return false;\n                }\n                if (action.view_id && action.view_id[1] === \"res.users.preferences.form.inherit\") {\n                    // The employee profile view is too complex to handle inside studio.\n                    // @see SELF_READABLE_FIELDS.\n                    return false;\n                }\n                return action.res_model ? true : false;\n            }\n            return false;\n        }\n\n        function isViewEditable(view) {\n            return view && supportedViewTypes.includes(view);\n        }\n\n        const bus = new EventBus();\n        let inStudio = false;\n\n        const menuSelectMenu = menu.selectMenu;\n        menu.selectMenu = async (argMenu) => {\n            if (!inStudio) {\n                return menuSelectMenu.call(menu, argMenu);\n            } else {\n                try {\n                    argMenu = typeof argMenu === \"number\" ? menu.getMenu(argMenu) : argMenu;\n                    await open(MODES.EDITOR, argMenu.actionID);\n                    menu.setCurrentMenu(argMenu);\n                } catch (e) {\n                    if (e instanceof NotEditableActionError) {\n                        notification.add(_t(\"This action is not editable by Studio\"), {\n                            type: \"danger\",\n                        });\n                        return;\n                    }\n                    throw e;\n                }\n            }\n        };\n\n        const state = {\n            studioMode: null,\n            editedViewType: null,\n            editedAction: null,\n            editedControllerState: null,\n            editorTab: \"views\",\n            editedReport: null,\n        };\n\n        async function _loadParamsFromURL() {\n            const urlState = router.current;\n            if (urlState.action === \"studio\") {\n                state.studioMode = urlState[URL_MODE_KEY];\n                state.editedViewType = urlState[URL_VIEW_KEY] || null;\n                const editorTab = urlState[URL_TAB_KEY] || null;\n                state.editorTab = editorTab;\n                if (editorTab === \"reports\") {\n                    const reportId = urlState[URL_REPORT_ID_KEY] || null;\n                    if (reportId) {\n                        state.editedReport = { res_id: reportId };\n                    }\n                }\n\n                const editedActionId = urlState[URL_ACTION_KEY];\n                const additionalContext = {};\n                if (state.studioMode === MODES.EDITOR) {\n                    const { active_id, active_ids } = urlState;\n                    if (active_id) {\n                        additionalContext.active_id = active_id;\n                        additionalContext.active_ids = [active_id];\n                    }\n                    if (active_ids) {\n                        additionalContext.active_ids = active_ids.split(\",\").map(Number);\n                    }\n                    if (editedActionId) {\n                        state.editedAction = await env.services.action.loadAction(\n                            editedActionId,\n                            additionalContext\n                        );\n                    } else {\n                        state.editedAction = null;\n                    }\n                }\n                if (!state.editedAction || !_isStudioEditable(state.editedAction)) {\n                    state.studioMode = state.studioMode || MODES.HOME_MENU;\n                    state.editedAction = null;\n                    state.editedViewType = null;\n                    state.editorTab = null;\n                }\n            }\n        }\n\n        const studioProm = _loadParamsFromURL();\n\n        async function _openStudio(targetMode, action = false, viewType = false) {\n            if (!targetMode) {\n                throw new Error(\"mode is mandatory\");\n            }\n\n            const previousState = { ...state };\n            const options = {};\n            if (targetMode === MODES.EDITOR) {\n                let controllerState;\n                if (!action) {\n                    // systray open\n                    const currentController = env.services.action.currentController;\n                    if (currentController) {\n                        action = currentController.action;\n                        viewType = currentController.view.type;\n                        controllerState = Object.assign({}, currentController.getLocalState());\n                        const { resIds } = currentController.getGlobalState() || {};\n                        controllerState.resIds = resIds || [controllerState.resId];\n                    }\n                }\n                if (!_isStudioEditable(action)) {\n                    throw new NotEditableActionError();\n                }\n                if (action !== state.editedAction) {\n                    options.clearBreadcrumbs = true;\n                    options.noEmptyTransition = true;\n                }\n                state.editedAction = action;\n                const vtype = viewType || action.views[0][1]; // fallback on first view of action\n                state.editedViewType = isViewEditable(vtype) ? vtype : null;\n                state.editorTab = \"views\";\n                state.editedControllerState = controllerState || {};\n            }\n            if (inStudio) {\n                options.stackPosition = \"replaceCurrentAction\";\n            }\n            state.studioMode = targetMode;\n\n            let res;\n            try {\n                res = await env.services.action.doAction(\"studio\", options);\n            } catch (e) {\n                Object.assign(state, previousState);\n                throw e;\n            }\n            return res;\n        }\n\n        async function open(mode = false, actionId = false) {\n            if (!mode && inStudio) {\n                throw new Error(\"can't already be in studio\");\n            }\n            if (!mode) {\n                mode = env.services.home_menu.hasHomeMenu ? MODES.HOME_MENU : MODES.EDITOR;\n            }\n            let action;\n            if (actionId) {\n                action = await env.services.action.loadAction(actionId);\n            }\n            resetViewCompilerCache();\n            return _openStudio(mode, action);\n        }\n\n        async function leave(actionId) {\n            if (!inStudio) {\n                throw new Error(\"leave when not in studio???\");\n            }\n            env.bus.trigger(\"CLEAR-CACHES\");\n\n            const options = {\n                onActionReady: () => resetViewCompilerCache(),\n                stackPosition: \"replacePreviousAction\", // If target is menu, then replaceCurrent, see comment above why we cannot do this\n            };\n            if (!actionId) {\n                if (state.studioMode === MODES.EDITOR) {\n                    actionId = state.editedAction.id;\n                    options.additionalContext = state.editedAction.context;\n                    options.viewType = state.editedViewType;\n                    if (state.editedControllerState) {\n                        options.props = { resId: state.editedControllerState.resId };\n                    }\n                } else {\n                    actionId = \"menu\";\n                }\n            }\n            await env.services.action.doAction(actionId, options);\n            // force rendering of the main navbar to allow adaptation of the size\n            env.bus.trigger(\"MENUS:APP-CHANGED\");\n            state.studioMode = null;\n        }\n\n        async function reload(params = {}, reset = true) {\n            resetViewCompilerCache();\n            env.bus.trigger(\"CLEAR-CACHES\");\n            const actionContext = state.editedAction.context;\n            let additionalContext;\n            if (actionContext.active_id) {\n                additionalContext = { active_id: actionContext.active_id };\n            }\n            if (actionContext.active_ids) {\n                additionalContext = Object.assign(additionalContext || {}, {\n                    active_ids: actionContext.active_ids,\n                });\n            }\n            const action = await env.services.action.loadAction(\n                state.editedAction.id,\n                additionalContext\n            );\n            setParams({ action, ...params }, reset);\n        }\n\n        function toggleHomeMenu() {\n            if (!inStudio) {\n                throw new Error(\"is it possible?\");\n            }\n            let targetMode;\n            if (state.studioMode === MODES.APP_CREATOR || state.studioMode === MODES.EDITOR) {\n                targetMode = MODES.HOME_MENU;\n            } else {\n                targetMode = MODES.EDITOR;\n            }\n            const action = targetMode === MODES.EDITOR ? state.editedAction : null;\n            if (targetMode === MODES.EDITOR && !action) {\n                throw new Error(\"this button should not be clickable/visible\");\n            }\n            const viewType = targetMode === MODES.EDITOR ? state.editedViewType : null;\n            return _openStudio(targetMode, action, viewType);\n        }\n\n        function pushState() {\n            const search = { action: \"studio\" };\n            search[URL_MODE_KEY] = state.studioMode;\n            search[URL_ACTION_KEY] = undefined;\n            search[URL_VIEW_KEY] = undefined;\n            search[URL_TAB_KEY] = undefined;\n            if (state.studioMode === MODES.EDITOR) {\n                search[URL_ACTION_KEY] = JSON.stringify(state.editedAction.id);\n                search[URL_VIEW_KEY] = state.editedViewType || undefined;\n                search[URL_TAB_KEY] = state.editorTab;\n            }\n            if (\n                state.editedAction &&\n                state.editedAction.context &&\n                state.editedAction.context.active_id\n            ) {\n                search.active_id = state.editedAction.context.active_id;\n            }\n\n            if (state.editorTab === \"reports\" && state.editedReport) {\n                search[URL_REPORT_ID_KEY] = state.editedReport.res_id;\n            }\n            router.pushState(search, { replace: true });\n        }\n\n        function setParams(params = {}, reset = true) {\n            if (\"mode\" in params) {\n                state.studioMode = params.mode;\n            }\n            if (\"viewType\" in params) {\n                state.editedViewType = params.viewType || null;\n            }\n            if (\"action\" in params) {\n                if ((state.editedAction && state.editedAction.id) !== params.action.id) {\n                    state.editedControllerState = null;\n                }\n                state.editedAction = params.action || null;\n            }\n            if (\"editorTab\" in params) {\n                state.editorTab = params.editorTab;\n                if (!(\"viewType\" in params)) {\n                    // clean me\n                    state.editedViewType = null;\n                }\n                if (!(\"editedReport\" in params)) {\n                    state.editedReport = null;\n                }\n            }\n            if (\"editedReport\" in params) {\n                state.editedReport = params.editedReport;\n            }\n            if (\"controllerState\" in params) {\n                state.editedControllerState = params.controllerState;\n            }\n            if (state.editorTab !== \"reports\") {\n                state.editedReport = null;\n            }\n            bus.trigger(\"UPDATE\", { reset });\n        }\n\n        env.bus.addEventListener(\"ACTION_MANAGER:UI-UPDATED\", (ev) => {\n            const mode = ev.detail;\n            if (mode === \"new\") {\n                return;\n            }\n            const action = _getCurrentAction();\n            inStudio = action.tag === \"studio\";\n        });\n\n        const isAllowedCache = {\n            activity: {},\n            chatter: {},\n        };\n\n        function isAllowed(type, resModel) {\n            if (!Object.keys(isAllowedCache).includes(type)) {\n                return;\n            }\n            let val;\n            if (resModel in isAllowedCache[type]) {\n                val = isAllowedCache[type][resModel];\n            } else {\n                val = rpc(`/web_studio/${type}_allowed`, { model: resModel });\n                isAllowedCache[type][resModel] = val;\n            }\n            return val;\n        }\n\n        return {\n            MODES,\n            bus,\n            isStudioEditable() {\n                const action = _getCurrentAction();\n                return action ? _isStudioEditable(action) : false;\n            },\n            open,\n            reload,\n            pushState,\n            leave,\n            toggleHomeMenu,\n            setParams,\n            get ready() {\n                return studioProm;\n            },\n            get mode() {\n                return state.studioMode;\n            },\n            get editedAction() {\n                return state.editedAction;\n            },\n            get editedViewType() {\n                return state.editedViewType;\n            },\n            get editedControllerState() {\n                return state.editedControllerState;\n            },\n            get editedReport() {\n                return state.editedReport;\n            },\n            get editorTab() {\n                return state.editorTab;\n            },\n            isAllowed,\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"studio\", studioService);\n\nexport function useStudioServiceAsReactive() {\n    const studio = useService(\"studio\");\n    const state = useState({ ...studio });\n    state.requestId = 1;\n\n    function onUpdate({ detail }) {\n        Object.assign(state, studio);\n        if (detail.reset) {\n            state.requestId++;\n        }\n    }\n    studio.bus.addEventListener(\"UPDATE\", onUpdate);\n    onWillUnmount(() => studio.bus.removeEventListener(\"UPDATE\", onUpdate));\n    return state;\n}\n\nfunction actionLeave(env, action) {\n    const actionId = action.context.action_id;\n    return env.services.studio.leave(actionId);\n}\n\nregistry.category(\"actions\").add(\"action_web_studio_leave_with\", actionLeave);\n", "/** @odoo-module default=false **/\nexport const COLORS = [\n    \"#FFFFFF\",\n    \"#262c34\",\n    \"#f1c40f\",\n    \"#FBB130\",\n    \"#FC787D\",\n    \"#EB5A46\",\n    \"#9b59b6\",\n    \"#0079BF\",\n    \"#1BB6F9\",\n    \"#4dd0e1\",\n    \"#00CEB3\",\n    \"#2ecc71\",\n];\n\nexport const BG_COLORS = [\n    \"#FFFFFF\",\n    \"#1abc9c\",\n    \"#58a177\",\n    \"#B4C259\",\n    \"#56829f\",\n    \"#636DA9\",\n    \"#34495e\",\n    \"#BC4242\",\n    \"#C6572A\",\n    \"#d49054\",\n    \"#D89F45\",\n    \"#DAB852\",\n    \"#606060\",\n    \"#6B6C70\",\n    \"#838383\",\n];\n\n/**\n * This allows to list Font Awesome icon, independently of the library version\n * Some icons use the same glyph for multiple classes. Those are filtered to only\n * list the icon once\n */\nexport function getFontAwesomeIcons() {\n    const styleSheet = [...document.styleSheets].find(\n        (s) => s && s.href && s.href.includes(\"/web/\")\n    );\n    const fontAwesomeStyles = [...styleSheet.cssRules]\n        .filter((e) => /^\\.fa-.*:before/.test(e.selectorText))\n        .filter((e) => e && e.style && e.style.length === 1 && e.style[0] === \"content\");\n    return fontAwesomeStyles.map((rule) => {\n        const classNames = rule.selectorText.split(/:?:before|,/).filter((e) => e.length > 1);\n        const searchTerms = classNames.map((selector) =>\n            selector.replace(\".fa-\", \"\").replace(/-o$/g, \" (Outline)\").replaceAll(\"-\", \" \")\n        );\n        return {\n            className: \"fa \" + classNames[0].slice(1),\n            searchTerms,\n            tooltip: searchTerms[0].charAt(0).toUpperCase() + searchTerms[0].slice(1),\n        };\n    });\n}\n\n/**\n * @param {Integer} string_length\n * @returns {String} A random string with numbers and lower/upper case chars\n */\nexport function randomString(string_length) {\n    var chars = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\";\n    var randomstring = \"\";\n    for (var i = 0; i < string_length; i++) {\n        var rnum = Math.floor(Math.random() * chars.length);\n        randomstring += chars.substring(rnum, rnum + 1);\n    }\n    return randomstring;\n}\n\nexport default {\n    BG_COLORS,\n    COLORS,\n    randomString,\n};\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { LazyComponent } from \"@web/core/assets\";\nimport { cookie } from \"@web/core/browser/cookie\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\n\nimport { Component, xml } from \"@odoo/owl\";\n\nclass StudioActionLoader extends Component {\n    static components = { LazyComponent };\n    static template = xml`\n        <LazyComponent bundle=\"bundle\" Component=\"'StudioClientAction'\" props=\"props\"/>\n    `;\n    static props = {\n        ...standardActionServiceProps,\n        props: { type: Object, optional: true },\n        Component: { type: Function, optional: true },\n    };\n    setup() {\n        this.bundle =\n            cookie.get(\"color_scheme\") === \"dark\"\n                ? \"web_studio.studio_assets_dark\"\n                : \"web_studio.studio_assets\";\n    }\n}\nregistry.category(\"actions\").add(\"studio\", StudioActionLoader);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\n\nconst actionRegistry = registry.category(\"actions\");\n\nactionRegistry.add(\"action_web_studio_app_creator\",\n    (env) => env.services.studio.open(env.services.studio.MODES.APP_CREATOR)\n);\n", "/** @odoo-module */\nimport { Component } from \"@odoo/owl\";\nimport { SelectMenu } from \"@web/core/select_menu/select_menu\";\nimport { getFontAwesomeIcons } from \"@web_studio/utils\";\n\nexport class FontAwesomeIconSelector extends Component {\n    static defaultProps = {\n        className: \"\",\n        menuClassName: \"\",\n    };\n    static template = \"web_studio.FontAwesomeIconSelector\";\n    static props = {\n        className: { type: String, optional: true },\n        menuClassName: { type: String, optional: true },\n        value: { type: String },\n        onSelect: { type: Function, optional: true },\n        slots: true,\n    };\n    static components = { SelectMenu };\n\n    setup() {\n        this.ICONS = getFontAwesomeIcons();\n    }\n\n    get iconChoices() {\n        return this.ICONS.map((icon) => {\n            return {\n                label: icon.searchTerms.join(\" \"),\n                value: icon.className,\n            };\n        });\n    }\n\n    getIconTooltip(value) {\n        return this.ICONS.find((icon) => icon.className === value).tooltip;\n    }\n}\n", "/** @odoo-module */\nimport { Component } from \"@odoo/owl\";\n\nexport class SidebarDraggableItem extends Component {\n    static template = \"web_studio.SidebarDraggableItem\";\n    static props = {\n        className: { type: String, optional: true },\n        description: { type: String, optional: true },\n        dropData: { optional: true },\n        string: { type: String },\n        structure: { type: String },\n    };\n}\n", "/** @odoo-module */\nimport { Component } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\n\nexport class ThumbnailItem extends Component {\n    static defaultProps = {\n        showMoreMenu: true,\n        onClick: () => {},\n    };\n    static template = \"web_studio.ThumbnailItem\";\n    static props = {\n        className: { type: String, optional: true },\n        showMoreMenu: { type: Boolean, optional: true },\n        icon: { type: Object },\n        onClick: { type: Function, optional: true },\n        slots: true,\n    };\n    static components = { Dropdown };\n\n    get hasDropdown() {\n        return this.props.showMoreMenu && this.props.slots && this.props.slots.dropdown;\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { download } from \"@web/core/network/download\";\nimport { pick } from \"@web/core/utils/objects\";\n\nasync function studioExportAction(_env, action) {\n    await download({\n        url: \"/web_studio/export\",\n        data: pick(action.context, [\"active_id\"]),\n    });\n}\n\nregistry.category(\"actions\").add(\"studio_export_action\", studioExportAction);\n", "/** @odoo-module **/\nimport { HomeMenu } from \"@web_enterprise/webclient/home_menu/home_menu\";\nimport { url } from \"@web/core/utils/urls\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { onMounted, onWillUnmount } from \"@odoo/owl\";\n\npatch(HomeMenu.prototype, {\n    setup() {\n        super.setup();\n        if (!this.menus.getMenu(\"root\").backgroundImage) {\n            return;\n        }\n        this.backgroundImageUrl = url(\"/web/image\", {\n            id: this.env.services.company.currentCompany.id,\n            model: \"res.company\",\n            field: \"background_image\",\n        });\n\n        onMounted(() => {\n            document.body.classList.add(\"o_home_menu_background\");\n            document.body.classList.toggle(\n                \"o_home_menu_background_custom\",\n                this.menus.getMenu(\"root\").backgroundImage\n            );\n        });\n\n        onWillUnmount(() => {\n            document.body.classList.remove(\n                \"o_home_menu_background\",\n                \"o_home_menu_background_custom\"\n            );\n        });\n    },\n});\n", "/** @odoo-module */\n\nimport { patch } from \"@web/core/utils/patch\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport \"@web_enterprise/views/list/list_renderer_desktop\";\n\nexport const patchListRendererStudio = () => ({\n    setup() {\n        super.setup(...arguments);\n        this.studioService = useService(\"studio\");\n    },\n    /**\n     * This function opens the studio mode with current view\n     *\n     * @override\n     */\n    onSelectedAddCustomField() {\n        this.studioService.open();\n    },\n\n    isStudioEditable() {\n        return !this.studioService.mode && super.isStudioEditable();\n    },\n});\n\nexport const unpatchListRendererStudio = patch(ListRenderer.prototype, patchListRendererStudio());\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { markup } from \"@odoo/owl\";\nimport utils from \"@web_studio/utils\";\n\nregistry.category(\"web_tour.tours\").add(\"web_studio_new_app_tour\", {\n    url: \"/odoo/action-studio?mode=home_menu\",\n    steps: () => [\n        {\n            trigger: \".o_web_studio_new_app\",\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_web_studio_app_creator_next\",\n            content: markup(\n                _t(\"I bet you can <b>build an app</b> in 5 minutes. Ready for the challenge?\")\n            ),\n            tooltipPosition: \"top\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_web_studio_app_creator_name > input\",\n            content: markup(_t(\"How do you want to <b>name</b> your app? Library, Academy, \u2026?\")),\n            tooltipPosition: \"right\",\n            run: \"edit \" + utils.randomString(6),\n        },\n        {\n            trigger: \".o_web_studio_selectors .o_web_studio_selector_icon > button\",\n            content: _t(\"Now, customize your icon. Make it yours.\"),\n            tooltipPosition: \"top\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_web_studio_app_creator_next.is_ready\",\n            content: _t(\"Go on, you are almost done!\"),\n            tooltipPosition: \"top\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_web_studio_menu_creator > input\",\n            content: markup(\n                _t(\"How do you want to name your first <b>menu</b>? My books, My courses?\")\n            ),\n            tooltipPosition: \"right\",\n            run: \"edit \" + utils.randomString(6),\n        },\n        {\n            trigger: \".o_web_studio_app_creator_next.is_ready\",\n            content: _t(\n                \"Continue to configure some typical behaviors for your new type of object.\"\n            ),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_web_studio_model_configurator_next\",\n            content: markup(\n                _t(\"All set? You are just one click away from <b>generating your first app</b>.\")\n            ),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger:\n                \".o_web_studio_sidebar .o_web_studio_field_type_container:eq(1) .o_web_studio_field_char\",\n            content: markup(\n                _t(\n                    \"Nicely done! Let's build your screen now; <b>drag</b> a <i>text field</i> and <b>drop</b> it in your view, on the right.\"\n                )\n            ),\n            tooltipPosition: \"bottom\",\n            run: \"drag_and_drop .o_web_studio_form_view_editor .o_inner_group\",\n            timeout: 60000 /* previous step reloads registry, etc. - could take a long time */,\n        },\n        {\n            trigger: \".o_web_studio_form_view_editor .o_wrap_label label\",\n            content: markup(_t(\"To <b>customize a field</b>, click on its <i>label</i>.\")),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: '.o_web_studio_sidebar input[name=\"string\"]',\n            content: markup(\n                _t(\n                    \"Here, you can <b>name</b> your field (e.g. Book reference, ISBN, Internal Note, etc.).\"\n                )\n            ),\n            tooltipPosition: \"bottom\",\n            run: \"edit My Field && click body\",\n        },\n        {\n            trigger: \".o_web_studio_form_view_editor .o_wrap_label label:contains(My Field)\",\n        },\n        {\n            // wait for the field to be renamed\n            trigger: \".o_web_studio_sidebar .o_web_studio_new\",\n            content: markup(\n                _t(\"Good job! To add more <b>fields</b>, come back to the <i>Add tab</i>.\")\n            ),\n            tooltipPosition: \"bottom\",\n            // the rename operation (/web_studio/rename_field + /web_studio/edit_view)\n            // takes a while and sometimes reaches the default 10s timeout\n            timeout: 20000,\n            run: \"click\",\n        },\n        {\n            trigger:\n                \".o_web_studio_sidebar .o_web_studio_field_type_container:eq(1) .o_web_studio_field_selection\",\n            content: markup(\n                _t(\"Drag & drop <b>another field</b>. Let's try with a <i>selection field</i>.\")\n            ),\n            tooltipPosition: \"bottom\",\n            run: \"drag_and_drop .o_web_studio_form_view_editor .o_inner_group\",\n        },\n        {\n            trigger: \".o_web_studio_selection_editor .o_web_studio_add_selection input\",\n            content: markup(\n                _t(\"Create your <b>selection values</b> (e.g.: Romance, Polar, Fantasy, etc.)\")\n            ),\n            tooltipPosition: \"top\",\n            run: \"edit \" + utils.randomString(6),\n        },\n        {\n            trigger:\n                \".o_web_studio_selection_editor .o_web_studio_add_selection .o-web-studio-interactive-list-edit-item\",\n            run: \"click\",\n        },\n        {\n            trigger: \".modal-footer > button:eq(0)\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_web_studio_form_view_editor .o-mail-Chatter\",\n            content: _t(\"Click to edit messaging features on your model.\"),\n            tooltipPosition: \"top\",\n            run: \"click\",\n            timeout: 15000, // this can take some time on 'slow' builds (coverage, etc.)\n        },\n        {\n            trigger: '.o_web_studio_sidebar input[name=\"email_alias\"]',\n            content: markup(\n                _t(\n                    \"Set an <b>email alias</b>. Then, try to send an email to this address; it will create a document automatically for you. Pretty cool, huh?\"\n                )\n            ),\n            tooltipPosition: \"bottom\",\n            run: \"edit Test\",\n        },\n        {\n            trigger: \".o_web_studio_leave\",\n            content: markup(\n                _t(\n                    \"Let's check the result. Close Odoo Studio to get an <b>overview of your app</b>.\"\n                )\n            ),\n            tooltipPosition: \"left\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_field_char.o_required_modifier > input\",\n            tooltipPosition: \"bottom\",\n            run: \"edit Test\",\n        },\n        {\n            trigger: \".o_control_panel .o_form_button_save\",\n            content: _t(\"Save.\"),\n            tooltipPosition: \"right\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_form_view .o_form_saved\",\n        },\n        {\n            trigger: \".o_web_studio_navbar_item\",\n            content: markup(\n                _t(\n                    \"Wow, nice! And I'm sure you can make it even better! Use this icon to open <b>Odoo Studio</b> and customize any screen.\"\n                )\n            ),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_web_studio_menu .o_menu_sections li:contains(Views)\",\n            content: markup(_t(\"Want more fun? Let's create more <b>views</b>.\")),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_web_studio_view_category .o_web_studio_thumbnail_kanban.disabled\",\n            content: markup(_t(\"What about a <b>Kanban view</b>?\")),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_web_studio_sidebar .o_web_studio_new\",\n            content: markup(_t(\"Now you're on your own. Enjoy your <b>super power</b>.\")),\n            tooltipPosition: \"bottom\",\n        },\n    ],\n});\n", "/** @odoo-module */\nimport { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport { reactive, useComponent, useEnv, toRaw, onMounted, onWillDestroy } from \"@odoo/owl\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { registry } from \"@web/core/registry\";\n\n/**\n * An customization of @web/core/timing/batched that allows two things:\n * - store call arguments as long as we have not passed the synchronize timing\n * - returns the same promise to each caller that will be resolved when callback has finished\n * @param {Function} callback\n * @param {Function} synchronize\n * @returns Function\n */\nfunction batched(callback, synchronize = () => Promise.resolve()) {\n    const map = new Map();\n    let callId = 0;\n    return (...args) => {\n        if (!map.has(callId)) {\n            const argsList = [args];\n            const prom = new Deferred();\n            map.set(callId, { argsList, prom });\n            synchronize().then(() => {\n                const currentCallId = callId++;\n                Promise.resolve(callback(...argsList))\n                    .then(prom.resolve)\n                    .catch(prom.reject)\n                    .finally(() => map.delete(currentCallId));\n            });\n            return prom;\n        } else {\n            const { prom, argsList } = map.get(callId);\n            argsList.push(args);\n            return prom;\n        }\n    };\n}\n\nexport function buildApprovalKey(...args) {\n    return args.join(\"-\");\n}\n\nexport const getApprovalSpecBatchedService = {\n    name: \"web_studio.get_approval_spec_batched\",\n    dependencies: [\"orm\"],\n    start(env, { orm }) {\n        return batched(async (...argsLists) => {\n            const approvals = await orm.silent.call(\"studio.approval.rule\", \"get_approval_spec\", [\n                argsLists.flat(),\n            ]);\n\n            for (const [key, tupleList] of Object.entries(approvals)) {\n                if (key === \"all_rules\") {\n                    continue;\n                }\n                approvals[key] = Object.fromEntries(\n                    tupleList.map(([tuple, value]) => {\n                        return [buildApprovalKey(...tuple), value];\n                    })\n                );\n            }\n\n            return approvals;\n        });\n    },\n};\n\nregistry\n    .category(\"services\")\n    .add(getApprovalSpecBatchedService.name, getApprovalSpecBatchedService);\n\nfunction getMissingApprovals(entries, rules) {\n    const missingApprovals = [];\n    const doneApprovals = entries.filter((e) => e.approved).map((e) => e.rule_id[0]);\n    rules.forEach((r) => {\n        if (!doneApprovals.includes(r.id)) {\n            missingApprovals.push(r);\n        }\n    });\n    return missingApprovals;\n}\n\nclass StudioApproval {\n    constructor({ getApprovalSpecBatched, model }) {\n        this._data = reactive({});\n        this.model = model;\n        this.rules = {};\n\n        const promSet = new WeakSet();\n        this.getApprovalSpecBatched = (...args) => {\n            const prom = getApprovalSpecBatched(...args);\n            if (!promSet.has(prom)) {\n                promSet.add(prom);\n                prom.then((approvals) => {\n                    Object.assign(this.rules, approvals.all_rules);\n                });\n            }\n            return prom;\n        };\n\n        // Lazy properties to be set by specialization.\n        this.orm = null;\n        this.studio = null;\n        this.notification = null;\n        this.resModel = null;\n        this.resId = null;\n        this.method = null;\n        this.action = null;\n    }\n\n    get dataKey() {\n        return buildApprovalKey(this.resModel, this.resId || false, this.method, this.action);\n    }\n\n    /**\n     * The approval's values for a given resModel, resId, method and action.\n     * If current values don't exist, we fetch them from the server. Owl's fine reactivity\n     * does the update of every component using that state.\n     */\n    get state() {\n        const state = this._getState();\n        if (state.rules === null && !state.syncing && !this.willCheck) {\n            this.fetchApprovals();\n        }\n        return state;\n    }\n\n    get inStudio() {\n        return this.studio;\n    }\n\n    displayNotification(data) {\n        const missingApprovals = getMissingApprovals(data.entries, data.rules);\n        this.notification.add(\n            missingApprovals.length > 1\n                ? _t(\"Some approvals are missing\")\n                : _t(\"An approval is missing\"),\n            {\n                type: \"warning\",\n            }\n        );\n    }\n\n    async checkApproval() {\n        const args = [this.resModel, this.resId, this.method, this.action];\n        const state = this._getState();\n        state.syncing = true;\n        const result = await this.orm.call(\"studio.approval.rule\", \"check_approval\", args);\n        const approved = result.approved;\n        if (!approved) {\n            this.displayNotification(result);\n        }\n        this.willCheck = false;\n        this.fetchApprovals(); // don't wait\n        return approved;\n    }\n\n    async fetchApprovals() {\n        const state = this._getState();\n        state.syncing = true;\n        // In studio we fetch every rule, even if they do not apply\n        // to the current record if present\n        const resId = !this.inStudio && this.resId;\n        try {\n            const allApprovals = await this.getApprovalSpecBatched({\n                model: this.resModel,\n                method: this.method,\n                action_id: this.action,\n                res_id: resId,\n            });\n            const myApproval = allApprovals[this.resModel][\n                buildApprovalKey(resId, this.method || false, this.action || false)\n            ] || { rules: [], entries: [] };\n            Object.assign(state, myApproval);\n        } catch {\n            Object.assign(state, { rules: [], entries: [] });\n        } finally {\n            state.syncing = false;\n        }\n    }\n\n    /**\n     * Create or update an approval entry for a specified rule server-side.\n     * @param {Number} ruleId\n     * @param {Boolean} approved\n     */\n    async setApproval(ruleId, approved) {\n        try {\n            await this.orm.call(\"studio.approval.rule\", \"set_approval\", [[ruleId]], {\n                res_id: this.resId,\n                approved,\n            });\n        } catch (e) {\n            this.fetchApprovals();\n            throw e;\n        }\n        return await this.model.root.load();\n    }\n\n    /**\n     * Delete an approval entry for a given rule server-side.\n     * @param {Number} ruleId\n     */\n    async cancelApproval(ruleId) {\n        try {\n            await this.orm.call(\"studio.approval.rule\", \"delete_approval\", [[ruleId]], {\n                res_id: this.resId,\n            });\n        } catch (e) {\n            this.fetchApprovals();\n            throw e;\n        }\n        return this.model.root.load();\n    }\n\n    _getState() {\n        if (!(this.dataKey in this._data)) {\n            this._data[this.dataKey] = { rules: null };\n        }\n        return this._data[this.dataKey];\n    }\n}\n\nconst approvalMap = new WeakMap();\n\nexport function useApproval({ getRecord, method, action }) {\n    /* The component using this hook can be destroyed before ever being mounted.\n    In practice, we do an rpc call in the component setup without knowing if it will be mounted.\n    When a new instance of the component is created, it will share the same data, and the\n    promise from `useService(\"orm\")` will never resolve due to the old instance being destroyed.\n    What we can do to prevent that, is initially use an unprotected orm and once\n    the component has been mounted, we can switch the orm to the one from useService. */\n    const protectedOrm = useService(\"orm\");\n    const unprotectedOrm = useEnv().services.orm;\n    const notification = useService(\"notification\");\n    const record = getRecord(useComponent().props);\n    const model = toRaw(record.model);\n    const getApprovalSpecBatched = useEnv().services[\"web_studio.get_approval_spec_batched\"];\n    let approvalModelCache = approvalMap.get(model);\n    if (!approvalModelCache) {\n        approvalModelCache = {\n            approval: new StudioApproval({ getApprovalSpecBatched, model }),\n            onRecordSaved: new Map(),\n        };\n        approvalMap.set(model, approvalModelCache);\n        const onRecordSaved = model.hooks.onRecordSaved;\n        model.hooks.onRecordSaved = (...args) => {\n            approvalModelCache.onRecordSaved.forEach((fn) => fn(args[0]));\n            return onRecordSaved(...args);\n        };\n        const onRootLoaded = model.hooks.onRootLoaded;\n        model.hooks.onRootLoaded = (...args) => {\n            // nullify every state. This will trigger a re-render and thus\n            // a fetch of all approval for buttons that ask for it.\n            for (const data of Object.values(approvalModelCache.approval._data)) {\n                data.rules = null;\n            }\n            if (onRootLoaded) {\n                return onRootLoaded(...args);\n            }\n        };\n    }\n\n    const specialize = {\n        resModel: record.resModel,\n        resId: record.resId,\n        method,\n        action,\n        orm: unprotectedOrm,\n        studio: !!record.context.studio,\n        notification,\n    };\n\n    const approval = reactive(\n        Object.assign(Object.create(approvalModelCache.approval), specialize)\n    );\n\n    approvalModelCache.onRecordSaved.set(toRaw(approval), () => {\n        if (!approval.resId && record.resId) {\n            approval.resId = record.resId;\n        } else {\n            delete approval._data[approval.dataKey];\n        }\n    });\n    onWillDestroy(() => approvalModelCache.onRecordSaved.delete(toRaw(approval)));\n\n    useRecordObserver((record) => {\n        approval.resId = record.resId;\n        approval.resModel = record.resModel;\n    });\n\n    onMounted(() => {\n        approval.orm = protectedOrm;\n    });\n\n    return approval;\n}\n", "/** @odoo-module */\n\nimport { formatDate, deserializeDate } from \"@web/core/l10n/dates\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { user } from \"@web/core/user\";\n\nimport { useState, Component, onWillRender } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { groupBy, sortBy } from \"@web/core/utils/arrays\";\n\nexport class StudioApprovalInfos extends Component {\n    static template = \"StudioApprovalInfos\";\n    static components = { Dialog };\n    static props = {\n        isPopover: Boolean,\n        approval: Object,\n        close: { type: Function, optional: true },\n    };\n\n    setup() {\n        this.user = user;\n        const approval = this.props.approval;\n        this.approval = approval;\n        this.state = useState(approval.state);\n        this.actionService = useService(\"action\");\n        onWillRender(() => {\n            this.ruleIdToEntry = Object.fromEntries(\n                this.state.entries.map((e) => [e.rule_id[0], e])\n            );\n\n            let ruleGrouped = groupBy(\n                Object.values(this.props.approval.rules),\n                \"notification_order\"\n            );\n            ruleGrouped = sortBy(\n                Object.entries(ruleGrouped),\n                ([key, group]) => parseInt(key),\n                \"desc\"\n            );\n\n            let canRevoke = false;\n            for (const group of ruleGrouped) {\n                const localCanRevoke = canRevoke;\n                group[1].forEach((r) => {\n                    r._canRevoke = localCanRevoke;\n                    canRevoke = canRevoke || r.can_validate;\n                });\n            }\n        });\n    }\n\n    formatDate(val, format) {\n        return formatDate(deserializeDate(val), { format });\n    }\n\n    getEntry(ruleId) {\n        return this.ruleIdToEntry[ruleId];\n    }\n\n    setApproval(ruleId, approved) {\n        return this.approval.setApproval(ruleId, approved);\n    }\n\n    canRevokeEntry(ruleId) {\n        const rule = this.props.approval.rules[ruleId];\n        const entry = this.getEntry(ruleId);\n        return entry.user_id[0] === this.user.userId || rule._canRevoke;\n    }\n\n    cancelApproval(ruleId) {\n        return this.approval.cancelApproval(ruleId);\n    }\n\n    openKanbanApprovalRules() {\n        const { resModel, method, action } = this.approval;\n        return this.actionService.doActionButton({\n            type: \"object\",\n            name: \"open_kanban_rules\",\n            resModel: \"studio.approval.rule\",\n            resIds: [],\n            args: JSON.stringify([resModel, method, action]),\n        });\n    }\n}\n", "import { useSubEnv } from \"@odoo/owl\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { FormController } from \"@web/views/form/form_controller\";\n\npatch(FormController.prototype, {\n    setup() {\n        super.setup();\n        if (this._shouldUseSubEnv()) {\n            const { relatedModels } = this.props;\n            const hasApprovalRules = {};\n            for (const model in relatedModels || {}) {\n                hasApprovalRules[model] = relatedModels[model].has_approval_rules || false;\n            }\n            useSubEnv({ hasApprovalRules });\n        }\n    },\n    _shouldUseSubEnv() {\n        return true;\n    },\n});\n", "/** @odoo-module */\n\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { StudioApprovalInfos } from \"@web_studio/approval/approval_infos\";\nimport { Component, onWillUnmount, useRef } from \"@odoo/owl\";\n\nfunction useOpenExternal() {\n    const closeFns = [];\n    function open(_open) {\n        const close = _open();\n        closeFns.push(close);\n        return close;\n    }\n\n    onWillUnmount(() => {\n        closeFns.forEach((cb) => cb());\n    });\n    return open;\n}\n\nexport class StudioApproval extends Component {\n    static props = {\n        approval: Object,\n    };\n    static template = \"StudioApproval\";\n\n    setup() {\n        this.dialog = useService(\"dialog\");\n        this.popover = usePopover(StudioApprovalInfos);\n        this.rootRef = useRef(\"root\");\n        this.openExternal = useOpenExternal();\n    }\n\n    get approval() {\n        return this.props.approval;\n    }\n\n    get state() {\n        return this.approval.state;\n    }\n\n    toggleApprovalInfo() {\n        if (this.env.isSmall) {\n            if (this.isOpened) {\n                this.closeInfos();\n                this.closeInfos = null;\n                return;\n            }\n            const onClose = () => {\n                this.isOpened = false;\n            };\n            this.closeInfos = this.openExternal(() =>\n                this.dialog.add(\n                    StudioApprovalInfos,\n                    { approval: this.approval, isPopover: false },\n                    { onClose }\n                )\n            );\n        } else {\n            this.popover.open(this.rootRef.el, { approval: this.approval, isPopover: true });\n        }\n    }\n\n    getEntry(ruleId) {\n        return this.state.entries.find((e) => e.rule_id[0] === ruleId);\n    }\n}\n", "import { useSubEnv } from \"@odoo/owl\";\nimport { rpcBus } from \"@web/core/network/rpc\";\nimport { UPDATE_METHODS } from \"@web/core/orm_service\";\nimport { registry } from \"@web/core/registry\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { ViewButton } from \"@web/views/view_button/view_button\";\nimport { useApproval } from \"@web_studio/approval/approval_hook\";\nimport { StudioApproval } from \"@web_studio/approval/studio_approval\";\n\nregistry.category(\"services\").add(\"clear_caches_on_approval_rules_change\", {\n    start(env) {\n        rpcBus.addEventListener(\"RPC:RESPONSE\", (ev) => {\n            const { model, method } = ev.detail.data.params;\n            if ([\"studio.approval.rule\"].includes(model) && UPDATE_METHODS.includes(method)) {\n                env.bus.trigger(\"CLEAR-CACHES\");\n            }\n        });\n    },\n});\n\npatch(ViewButton.prototype, {\n    setup() {\n        super.setup(...arguments);\n        if (this._shouldUseApproval()) {\n            let { type, name } = this.props.clickParams;\n            if (type && type.endsWith(\"=\")) {\n                type = type.slice(0, -1);\n            }\n            const action = type === \"action\" && name;\n            const method = type === \"object\" && name;\n            this.approval = useApproval({\n                getRecord: (props) => props.record,\n                action,\n                method,\n            });\n\n            const onClickViewButton = this.env.onClickViewButton;\n            useSubEnv({\n                onClickViewButton: (params) => {\n                    if (params.clickParams.type === \"action\") {\n                        // if the button is an action then we check the approval client side\n                        params.beforeExecute = this.checkBeforeExecute.bind(this);\n                    }\n                    onClickViewButton(params);\n                },\n            });\n        }\n    },\n\n    _shouldUseApproval() {\n        const { name } = this.props.clickParams || {};\n        const { resModel } = this.props.record || {};\n        return name && resModel && this.env.hasApprovalRules?.[resModel];\n    },\n\n    async checkBeforeExecute() {\n        this.approval.willCheck = true;\n        if (!this.approval.resId) {\n            const model = this.props.record.model;\n            const rec = \"resId\" in model.root ? model.root : this.props.record;\n            await rec.save();\n        } else if (this.props.record && this.props.record.isDirty) {\n            await this.props.record.save();\n        }\n        return this.approval.checkApproval();\n    },\n});\n\nViewButton.props.push(\"studioApproval?\");\nViewButton.components = Object.assign(ViewButton.components || {}, { StudioApproval });\n", "/** @odoo-module **/\n\nimport { onWillStart } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useOpenChat } from \"@mail/core/web/open_chat_hook\";\nimport { AvatarCardPopover } from \"@mail/discuss/web/avatar_card/avatar_card_popover\";\n\nexport class AvatarCardResourcePopover extends AvatarCardPopover {\n    static template = \"resource_mail.AvatarCardResourcePopover\";\n\n    static props = {\n        ...AvatarCardPopover.props,\n        recordModel: {\n            type: String,\n            optional: true,\n        },\n    };\n\n    static defaultProps = {\n        ...AvatarCardPopover.defaultProps,\n        recordModel: \"resource.resource\",\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.actionService = useService(\"action\");\n        this.openChat = useOpenChat(\"res.users\");\n        onWillStart(this.onWillStart);\n    }\n\n    async onWillStart() {\n        [this.record] = await this.orm.read(this.props.recordModel, [this.props.id], this.fieldNames);\n        await Promise.all(this.loadAdditionalData());\n    }\n\n    loadAdditionalData() {\n        // To use when overriden in other modules to load additional data, returns promise(s)\n        return [];\n    }\n\n    get fieldNames() {\n        const excludedFields = new Set([\"partner_id\"]);\n        return super.fieldNames\n            .concat([\"user_id\", \"resource_type\"])\n            .filter((field) => !excludedFields.has(field));\n    }\n\n    get email() {\n        return this.record.email;\n    }\n\n    get phone() {\n        return this.record.phone;\n    }\n\n    get displayAvatar() {\n        return this.record.user_id?.length;\n    }\n\n    get showViewProfileBtn() {\n        return false;\n    }\n\n    get userId() {\n        return this.record.user_id[0];\n    }\n}\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport {\n    Many2ManyTagsAvatarUserField,\n    many2ManyTagsAvatarUserField,\n    Many2ManyAvatarUserTagsList,\n} from \"@mail/views/web/fields/many2many_avatar_user_field/many2many_avatar_user_field\";\nimport { AvatarMany2XAutocomplete } from \"@web/views/fields/relational_utils\";\nimport { AvatarCardResourcePopover } from \"@resource_mail/components/avatar_card_resource/avatar_card_resource_popover\";\nimport { Domain } from \"@web/core/domain\";\n\n\nexport class AvatarResourceMany2XAutocomplete extends AvatarMany2XAutocomplete {\n    get optionsSource() {\n        return {\n            ...super.optionsSource,\n            optionTemplate: \"resource_mail.AvatarResourceMany2XAutocomplete\",\n        };\n    }\n\n    /**\n     * @override\n     */\n    search(request) {\n        return this.orm.call(\n            this.props.resModel,\n            \"search_read\",\n            [this.getDomain(request), [\"id\", \"display_name\", \"resource_type\", \"color\"]],\n            {\n                context: this.props.context,\n                limit: this.props.searchLimit + 1,\n            }\n        );\n    }\n\n    /**\n     * @override\n     */\n    getDomain(request) {\n        return Domain.and([[[\"name\", \"ilike\", request]], this.props.getDomain()]).toList(\n            this.props.context\n        );\n    }\n\n    /**\n     * @override\n     */\n    mapRecordToOption(result) {\n        return {\n            resModel: this.props.resModel,\n            value: result.id,\n            resourceType: result.resource_type,\n            label: result.display_name,\n            color: result.color,\n        };\n    }\n}\n\nclass Many2ManyAvatarResourceTagsList extends Many2ManyAvatarUserTagsList {\n    static template = \"resource_mail.Many2ManyAvatarResourceTagsList\";\n}\n\nexport class Many2ManyAvatarResourceField extends Many2ManyTagsAvatarUserField {\n    setup() {\n        super.setup(...arguments);\n        if (this.relation == \"resource.resource\") {\n            this.avatarCard = usePopover(AvatarCardResourcePopover);\n        }\n    }\n\n    static components = {\n        ...super.components,\n        Many2XAutocomplete: AvatarResourceMany2XAutocomplete,\n        TagsList: Many2ManyAvatarResourceTagsList,\n    };\n\n    displayAvatarCard(record) {\n        return !this.env.isSmall && this.relation === \"resource.resource\" && record.data.resource_type === \"user\";\n    }\n\n    getTagProps(record) {\n        return {\n            ...super.getTagProps(...arguments),\n            icon: record.data.resource_type === \"user\" ? null : \"fa-wrench\",\n            img: record.data.resource_type === \"user\"\n                ? `/web/image/${this.relation}/${record.resId}/avatar_128`\n                : null,\n        };\n    }\n}\n\nexport const many2ManyAvatarResourceField = {\n    ...many2ManyTagsAvatarUserField,\n    component: Many2ManyAvatarResourceField,\n    additionalClasses: [\"o_field_many2many_tags_avatar\"],\n    relatedFields: (fieldInfo) => {\n        return [\n            ...many2ManyTagsAvatarUserField.relatedFields(fieldInfo),\n            {\n                name: \"resource_type\",\n                type: \"selection\",\n            },\n        ];\n    },\n};\n\nregistry.category(\"fields\").add(\"many2many_avatar_resource\", many2ManyAvatarResourceField);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport {\n    Many2OneAvatarUserField,\n    many2OneAvatarUserField,\n} from \"@mail/views/web/fields/many2one_avatar_user_field/many2one_avatar_user_field\";\nimport { AvatarCardResourcePopover } from \"@resource_mail/components/avatar_card_resource/avatar_card_resource_popover\";\nimport { AvatarResourceMany2XAutocomplete } from \"@resource_mail/views/fields/many2many_avatar_resource/many2many_avatar_resource_field\";\n\n\nconst ExtendMany2OneAvatarToResource = (T) => class extends T {\n    // We choose to extend Many2One_avatar_user instead of patching it as field dependencies need to be added on the widget to manage resources\n    setup() {\n        super.setup();\n        this.avatarCard = usePopover(AvatarCardResourcePopover);\n    }\n\n    get displayAvatarCard() {\n        return !this.env.isSmall && this.relation === \"resource.resource\" && this.props.record.data.resource_type === \"user\";\n    }\n};\n\n\nexport class Many2OneAvatarResourceField extends ExtendMany2OneAvatarToResource(Many2OneAvatarUserField) {\n    static template = \"resource_mail.Many2OneAvatarResourceField\";\n    static components = {\n        ...super.components,\n        Many2XAutocomplete: AvatarResourceMany2XAutocomplete,\n    };\n}\n\nexport const many2OneAvatarResourceField = {\n    ...many2OneAvatarUserField,\n    component: Many2OneAvatarResourceField,\n    fieldDependencies: [\n        {\n            name: \"resource_type\", //to add in model that will use this widget for m2o field related to resource.resource record (as related field is only supported for x2m)\n            type: \"selection\",\n        },\n    ],\n};\n\nregistry.category(\"fields\").add(\"many2one_avatar_resource\", many2OneAvatarResourceField);\n\nexport class KanbanMany2OneAvatarResourceField extends ExtendMany2OneAvatarToResource(Many2OneAvatarResourceField) {\n    static template = \"resource_mail.KanbanMany2OneAvatarResourceField\";\n}\n\nexport const kanbanMany2OneAvatarResourceField = {\n    ...many2OneAvatarResourceField,\n    component: KanbanMany2OneAvatarResourceField,\n};\n\nregistry.category(\"fields\").add(\"kanban.many2one_avatar_resource\", kanbanMany2OneAvatarResourceField);\n", "/* @odoo-module */\n\nimport { patch } from \"@web/core/utils/patch\";\nimport { AvatarCardPopover } from \"@mail/discuss/web/avatar_card/avatar_card_popover\";\n\nexport const patchAvatarCardPopover = {\n    setup() {\n        super.setup();\n        this.userInfoTemplate = \"hr.avatarCardUserInfos\";\n    },\n    get fieldNames() {\n        const fields = super.fieldNames;\n        return fields.concat([\n            \"work_phone\",\n            \"work_email\",\n            \"work_location_name\",\n            \"work_location_type\",\n            \"job_title\",\n            \"department_id\",\n            this.props.recordModel ? \"employee_id\" : \"employee_ids\",\n        ]);\n    },\n    get email() {\n        return this.user.work_email || this.user.email;\n    },\n    get phone() {\n        return this.user.work_phone || this.user.phone;\n    },\n    async getProfileAction() {\n        return this.user.employee_ids?.length > 0\n            ? this.orm.call(\"hr.employee\", \"get_formview_action\", [this.user.employee_ids[0]])\n            : super.getProfileAction(...arguments);\n    },\n};\n\nexport const unpatchAvatarCardPopover = patch(AvatarCardPopover.prototype, patchAvatarCardPopover);\n", "/** @odoo-module **/\n\nimport { AvatarCardResourcePopover } from \"@resource_mail/components/avatar_card_resource/avatar_card_resource_popover\";\n\nexport class AvatarCardEmployeePopover extends AvatarCardResourcePopover {\n    static defaultProps = {\n        ...AvatarCardResourcePopover.defaultProps,\n        recordModel: \"hr.employee\",\n    };\n    async onWillStart() {\n        await super.onWillStart();\n        this.record.employee_id = [this.props.id];\n    }\n\n    get fieldNames() {\n        const excludedFields = [\"employee_id\", \"resource_type\"];\n        return super.fieldNames.filter((field) => !excludedFields.includes(field));\n    }\n}\n", "/* @odoo-module */\n\nimport { patch } from \"@web/core/utils/patch\";\nimport { AvatarCardResourcePopover } from \"@resource_mail/components/avatar_card_resource/avatar_card_resource_popover\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { TagsList } from \"@web/core/tags_list/tags_list\";\n\nconst patchAvatarCardResourcePopover = {\n    setup() {\n        super.setup();\n        (this.userInfoTemplate = \"hr.avatarCardResourceInfos\"),\n            (this.actionService = useService(\"action\"));\n    },\n    get fieldNames() {\n        return [...super.fieldNames, \"show_hr_icon_display\", \"hr_icon_display\"];\n    },\n    get email() {\n        return this.record.work_email || this.record.email;\n    },\n    get phone() {\n        return this.record.work_phone || this.record.phone;\n    },\n    get showViewProfileBtn() {\n        return this.record.employee_id?.length > 0;\n    },\n    async getProfileAction() {\n        return await this.orm.call(\"hr.employee\", \"get_formview_action\", [\n            this.record.employee_id[0],\n        ]);\n    },\n};\n\npatch(AvatarCardResourcePopover.prototype, patchAvatarCardResourcePopover);\n// Adding TagsList component allows display tag lists on the resource/employee avatar card\n// This is used by multiple modules depending on hr (planning for roles and hr_skills for skills)\npatch(AvatarCardResourcePopover, {\n    components: {\n        ...AvatarCardResourcePopover.components,\n        TagsList,\n    },\n});\n", "/** @odoo-module */\n\nimport { registry } from '@web/core/registry';\n\nimport { ImageField, imageField } from '@web/views/fields/image/image_field';\n\nexport class BackgroundImageField extends ImageField {\n    static template = \"hr.BackgroundImage\";\n}\n\nexport const backgroundImageField = {\n    ...imageField,\n    component: BackgroundImageField,\n};\n\nregistry.category(\"fields\").add(\"background_image\", backgroundImageField);\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { onWillStart, useState, onWillUpdateProps, Component } from \"@odoo/owl\";\n\nexport class DepartmentChart extends Component {\n    static template = \"hr.DepartmentChart\";\n    static props = {\n        ...standardWidgetProps,\n    };\n\n    setup() {\n        super.setup();\n\n        this.action = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.state = useState({\n            hierarchy: {},\n        });\n        onWillStart(async () => await this.fetchHierarchy(this.props.record.resId));\n\n        onWillUpdateProps(async (nextProps) => {\n            await this.fetchHierarchy(nextProps.record.resId);\n        });\n    }\n\n    async fetchHierarchy(departmentId) {\n        this.state.hierarchy = await this.orm.call(\"hr.department\", \"get_department_hierarchy\", [\n            departmentId,\n        ]);\n    }\n\n    async openDepartmentEmployees(departmentId) {\n        const dialogAction = await this.orm.call(\n            this.props.record.resModel,\n            \"action_employee_from_department\",\n            [departmentId],\n            {}\n        );\n        this.action.doAction(dialogAction);\n    }\n}\n\nexport const departmentChart = {\n    component: DepartmentChart,\n};\nregistry.category(\"view_widgets\").add(\"hr_department_chart\", departmentChart);\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\n\nimport { useOpenChat } from \"@mail/core/web/open_chat_hook\";\nimport { Component } from \"@odoo/owl\";\n\nexport class HrEmployeeChat extends Component {\n    static props = {\n        ...standardWidgetProps,\n    };\n    static template = \"hr.OpenChat\";\n\n    setup() {\n        super.setup();\n        this.openChat = useOpenChat(this.props.record.resModel);\n    }\n}\n\nexport const hrEmployeeChat = {\n    component: HrEmployeeChat,\n};\nregistry.category(\"view_widgets\").add(\"hr_employee_chat\", hrEmployeeChat);\n", "/** @odoo-module */\n\nimport { Component } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nexport class HrPresenceStatus extends Component {\n    static template = \"hr.HrPresenceStatus\";\n    static props = {\n        ...standardFieldProps,\n        tag: { type: String, optional: true },\n    };\n    static defaultProps = {\n        tag: \"small\",\n    };\n\n    get classNames() {\n        const classNames = [\"fa\"];\n        classNames.push(\n            this.icon,\n            \"fa-fw\",\n            \"o_button_icon\",\n            \"hr_presence\",\n            \"align-middle\",\n            this.color,\n        )\n        return classNames.join(\" \");\n    }\n\n    get color() {\n        switch (this.value) {\n            case \"presence_present\":\n                return \"text-success\";\n            case \"presence_absent\":\n                return \"o_icon_employee_absent\";\n            case \"presence_out_of_working_hour\":\n            case \"presence_archive\":\n                return \"text-muted\";\n            default:\n                return \"\";\n        }\n    }\n\n    get icon() {\n        return `fa-circle${this.value.startsWith(\"presence_archive\") ? \"-o\" : \"\"}`;\n    }\n\n    get label() {\n        return this.value !== false\n            ? this.options.find(([value, label]) => value === this.value)[1]\n            : \"\";\n    }\n\n    get options() {\n        return this.props.record.fields[this.props.name].selection.filter(\n            (option) => option[0] !== false && option[1] !== \"\"\n        );\n    }\n\n    get value() {\n        return this.props.record.data[this.props.name];\n    }\n}\n\nexport const hrPresenceStatus = {\n    component: HrPresenceStatus,\n    displayName: _t(\"HR Presence Status\"),\n    extractProps({ viewType }, dynamicInfo) {\n        return {\n            tag: viewType === \"kanban\" ? \"span\" : \"small\",\n        };\n    },\n};\n\nregistry.category(\"fields\").add(\"hr_presence_status\", hrPresenceStatus)\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { HrPresenceStatus, hrPresenceStatus } from \"../hr_presence_status/hr_presence_status\";\n\nexport class HrPresenceStatusPrivate extends HrPresenceStatus { }\n\nexport const hrPresenceStatusPrivate = {\n    ...hrPresenceStatus,\n    component: HrPresenceStatusPrivate,\n};\n\nregistry.category(\"fields\").add(\"hr_presence_status_private\", hrPresenceStatusPrivate);\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { RadioField, radioField } from \"@web/views/fields/radio/radio_field\";\n\nclass RadioImageField extends RadioField {\n    static template = \"hr_homeworking.RadioImageField\";\n}\n\nregistry.category(\"fields\").add(\"hr_homeworking_radio_image\", {\n    ...radioField,\n    component: RadioImageField,\n});\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { BinaryField, binaryField } from \"@web/views/fields/binary/binary_field\";\n\nexport class WorkPermitUploadField extends BinaryField {\n    static template = \"hr.WorkPermitUploadField\";\n}\n\nexport const workPermitUploadField = {\n    ...binaryField,\n    component: WorkPermitUploadField,\n};\n\nregistry.category(\"fields\").add(\"work_permit_upload\", workPermitUploadField);\n", "/* @odoo-module */\n\nimport { Persona } from \"@mail/core/common/persona_model\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Persona.prototype, {\n    employeeId: undefined,\n});\n", "/* @odoo-module */\n\nimport { threadActionsRegistry } from \"@mail/core/common/thread_actions\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useComponent } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nthreadActionsRegistry.add(\"open-hr-profile\", {\n    condition(component) {\n        return (\n            component.thread?.channel_type === \"chat\" &&\n            component.props.chatWindow?.isOpen &&\n            component.thread.correspondent?.persona.employeeId\n        );\n    },\n    icon: \"fa fa-fw fa-id-card\",\n    name: _t(\"Open Profile\"),\n    async open(component) {\n        component.actionService.doAction({\n            type: \"ir.actions.act_window\",\n            res_id: component.thread.correspondent.persona.employeeId,\n            res_model: \"hr.employee.public\",\n            views: [[false, \"form\"]],\n        });\n    },\n    async setup(action) {\n        const component = useComponent();\n        const orm = useService(\"orm\");\n        let employeeId;\n        if (\n            !component.thread?.correspondent?.persona.employeeId &&\n            component.thread?.correspondent\n        ) {\n            const employees = await orm.silent.searchRead(\n                \"hr.employee\",\n                [[\"user_partner_id\", \"=\", component.thread.correspondent.persona.id]],\n                [\"id\"]\n            );\n            employeeId = employees[0]?.id;\n            if (employeeId) {\n                component.thread.correspondent.persona.employeeId = employeeId;\n            }\n        }\n    },\n    sequence: 16,\n});\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Store } from \"@mail/core/common/store_service\";\nimport { patch } from \"@web/core/utils/patch\";\n\n/** @type {import(\"models\").Store} */\nconst storeServicePatch = {\n    setup() {\n        super.setup();\n        this.employees = {};\n    },\n    async getChat(person) {\n        const { employeeId } = person;\n        if (!employeeId) {\n            return super.getChat(person);\n        }\n        let employee = this.employees[employeeId];\n        if (!employee) {\n            this.employees[employeeId] = { id: employeeId };\n            employee = this.employees[employeeId];\n        }\n        if (!employee.user_id && !employee.hasCheckedUser) {\n            employee.hasCheckedUser = true;\n            const [employeeData] = await this.env.services.orm.silent.read(\n                \"hr.employee.public\",\n                [employee.id],\n                [\"user_id\", \"user_partner_id\"],\n                { context: { active_test: false } }\n            );\n            if (employeeData) {\n                employee.user_id = employeeData.user_id[0];\n                let user = this.users[employee.user_id];\n                if (!user) {\n                    this.users[employee.user_id] = { id: employee.user_id };\n                    user = this.users[employee.user_id];\n                }\n                user.partner_id = employeeData.user_partner_id[0];\n                this.Persona.insert({\n                    displayName: employeeData.user_partner_id[1],\n                    id: employeeData.user_partner_id[0],\n                    type: \"partner\",\n                });\n            }\n        }\n        if (!employee.user_id) {\n            this.env.services.notification.add(\n                _t(\"You can only chat with employees that have a dedicated user.\"),\n                { type: \"info\" }\n            );\n            return;\n        }\n        return super.getChat({ userId: employee.user_id });\n    },\n};\n\npatch(Store.prototype, storeServicePatch);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { preferencesItem } from \"@web/webclient/user_menu/user_menu_items\";\n\nexport function hrPreferencesItem(env)  {\n    return Object.assign(\n        {}, \n        preferencesItem(env),\n        {\n            description: _t('My Profile'),\n        }\n    );\n}\n\nregistry.category(\"user_menuitems\").add('profile', hrPreferencesItem, { force: true })\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useComponent } from \"@odoo/owl\";\n\nexport function useArchiveEmployee() {\n    const component = useComponent();\n    const action = useService(\"action\");\n    return (id) => {\n        action.doAction(\n            {\n                type: \"ir.actions.act_window\",\n                name: _t(\"Employee Termination\"),\n                res_model: \"hr.departure.wizard\",\n                views: [[false, \"form\"]],\n                view_mode: \"form\",\n                target: \"new\",\n                context: {\n                    active_id: id,\n                    toggle_active: true,\n                },\n            },\n            {\n                onClose: async () => {\n                    await component.model.load();\n                },\n            }\n        );\n    };\n}\n", "/** @odoo-module **/\n\nimport { onWillStart } from \"@odoo/owl\";\nimport { user } from \"@web/core/user\";\n\n/**\n * Mixin that handles public/private access of employee records in many2X fields\n * @param { Class } fieldClass\n * @returns Class\n */\nexport function EmployeeFieldRelationMixin(fieldClass) {\n    return class extends fieldClass {\n        static props = {\n            ...fieldClass.props,\n            relation: { type: String, optional: true },\n        };\n\n        setup() {\n            super.setup();\n            onWillStart(async () => {\n                this.isHrUser = await user.hasGroup(\"hr.group_hr_user\");\n            });\n        }\n\n        get relation() {\n            if (this.props.relation) {\n                return this.props.relation;\n            }\n            return this.isHrUser ? \"hr.employee\" : \"hr.employee.public\";\n        }\n    };\n}\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport {\n    Many2ManyTagsAvatarUserField,\n    KanbanMany2ManyTagsAvatarUserField,\n    many2ManyTagsAvatarUserField,\n    kanbanMany2ManyTagsAvatarUserField,\n    listMany2ManyTagsAvatarUserField,\n} from \"@mail/views/web/fields/many2many_avatar_user_field/many2many_avatar_user_field\";\nimport { EmployeeFieldRelationMixin } from \"@hr/views/fields/employee_field_relation_mixin\";\n\nexport class Many2ManyTagsAvatarEmployeeField extends EmployeeFieldRelationMixin(\n    Many2ManyTagsAvatarUserField\n) {}\n\nexport const many2ManyTagsAvatarEmployeeField = {\n    ...many2ManyTagsAvatarUserField,\n    component: Many2ManyTagsAvatarEmployeeField,\n    additionalClasses: [\n        ...many2ManyTagsAvatarUserField.additionalClasses,\n        \"o_field_many2many_avatar_user\",\n    ],\n    extractProps: (fieldInfo, dynamicInfo) => ({\n        ...many2ManyTagsAvatarUserField.extractProps(fieldInfo, dynamicInfo),\n        canQuickCreate: false,\n        relation: fieldInfo.options?.relation,\n    }),\n};\n\nregistry.category(\"fields\").add(\"many2many_avatar_employee\", many2ManyTagsAvatarEmployeeField);\n\nexport class KanbanMany2ManyTagsAvatarEmployeeField extends EmployeeFieldRelationMixin(\n    KanbanMany2ManyTagsAvatarUserField\n) {}\n\nexport const kanbanMany2ManyTagsAvatarEmployeeField = {\n    ...kanbanMany2ManyTagsAvatarUserField,\n    component: KanbanMany2ManyTagsAvatarEmployeeField,\n    additionalClasses: [\n        ...kanbanMany2ManyTagsAvatarUserField.additionalClasses,\n        \"o_field_many2many_avatar_user\",\n    ],\n    extractProps: (fieldInfo, dynamicInfo) => ({\n        ...kanbanMany2ManyTagsAvatarUserField.extractProps(fieldInfo, dynamicInfo),\n        relation: fieldInfo.options?.relation,\n    }),\n};\n\nregistry\n    .category(\"fields\")\n    .add(\"kanban.many2many_avatar_employee\", kanbanMany2ManyTagsAvatarEmployeeField);\nexport const listMany2ManyTagsAvatarEmployeeField = {\n    ...listMany2ManyTagsAvatarUserField,\n    additionalClasses: [\n        ...listMany2ManyTagsAvatarUserField.additionalClasses,\n        \"o_field_many2many_avatar_user\",\n    ],\n};\nregistry\n    .category(\"fields\")\n    .add(\"list.many2many_avatar_employee\", listMany2ManyTagsAvatarEmployeeField);\n", "/** @odoo-module **/\n\nimport { patch } from \"@web/core/utils/patch\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { AvatarCardEmployeePopover } from \"@hr/components/avatar_card_employee/avatar_card_employee_popover\";\nimport {\n    Many2ManyTagsAvatarUserField,\n    ListMany2ManyTagsAvatarUserField,\n    KanbanMany2ManyTagsAvatarUserField,\n} from \"@mail/views/web/fields/many2many_avatar_user_field/many2many_avatar_user_field\";\n\nconst many2manyAvatarUserPatch = {\n    setup() {\n        super.setup(...arguments);\n        if ([\"hr.employee\", \"hr.employee.public\"].includes(this.relation)) {\n            this.avatarCard = usePopover(AvatarCardEmployeePopover, { closeOnClickAway: true });\n        }\n    },\n    displayAvatarCard(record) {\n        return (\n            (!this.env.isSmall && [\"hr.employee\", \"hr.employee.public\"].includes(this.relation)) ||\n            super.displayAvatarCard\n        );\n    },\n    getAvatarCardProps(record) {\n        const originalProps = super.getAvatarCardProps(record);\n        if ([\"hr.employee\", \"hr.employee.public\"].includes(this.relation)) {\n            return {\n                ...originalProps,\n                recordModel: this.relation,\n            };\n        }\n        return originalProps;\n    },\n};\n\npatch(Many2ManyTagsAvatarUserField.prototype, many2manyAvatarUserPatch);\npatch(KanbanMany2ManyTagsAvatarUserField.prototype, many2manyAvatarUserPatch);\npatch(ListMany2ManyTagsAvatarUserField.prototype, many2manyAvatarUserPatch);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport {\n    Many2OneAvatarUserField,\n    KanbanMany2OneAvatarUserField,\n    many2OneAvatarUserField,\n    kanbanMany2OneAvatarUserField,\n} from \"@mail/views/web/fields/many2one_avatar_user_field/many2one_avatar_user_field\";\nimport { EmployeeFieldRelationMixin } from \"@hr/views/fields/employee_field_relation_mixin\";\n\n\nexport class Many2OneAvatarEmployeeField extends EmployeeFieldRelationMixin(\n    Many2OneAvatarUserField\n) {\n    get many2OneProps() {\n        return {\n            ...super.many2OneProps,\n            relation: this.relation,\n        };\n    }\n}\n\nexport const many2OneAvatarEmployeeField = {\n    ...many2OneAvatarUserField,\n    component: Many2OneAvatarEmployeeField,\n    additionalClasses: [\n        ...many2OneAvatarUserField.additionalClasses,\n        \"o_field_many2one_avatar_user\",\n    ],\n    extractProps: (fieldInfo, dynamicInfo) => ({\n        ...many2OneAvatarUserField.extractProps(fieldInfo, dynamicInfo),\n        canQuickCreate: false,\n        relation: fieldInfo.options?.relation,\n    }),\n};\n\nregistry.category(\"fields\").add(\"many2one_avatar_employee\", many2OneAvatarEmployeeField);\n\nexport class KanbanMany2OneAvatarEmployeeField extends EmployeeFieldRelationMixin(\n    KanbanMany2OneAvatarUserField\n) {\n    get many2OneProps() {\n        return {\n            ...super.many2OneProps,\n            relation: this.relation,\n        };\n    }\n}\n\nexport const kanbanMany2OneAvatarEmployeeField = {\n    ...kanbanMany2OneAvatarUserField,\n    component: KanbanMany2OneAvatarEmployeeField,\n    extractProps: (fieldInfo, dynamicInfo) => ({\n        ...kanbanMany2OneAvatarUserField.extractProps(fieldInfo, dynamicInfo),\n        relation: fieldInfo.options?.relation,\n    }),\n};\n\nregistry\n    .category(\"fields\")\n    .add(\"kanban.many2one_avatar_employee\", kanbanMany2OneAvatarEmployeeField);\n", "/** @odoo-module **/\n\nimport { patch } from \"@web/core/utils/patch\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport {\n    Many2OneAvatarUserField,\n    KanbanMany2OneAvatarUserField,\n} from \"@mail/views/web/fields/many2one_avatar_user_field/many2one_avatar_user_field\";\nimport { AvatarCardEmployeePopover } from \"@hr/components/avatar_card_employee/avatar_card_employee_popover\";\n\nexport const patchMany2OneAvatarUserField = {\n    setup() {\n        super.setup(...arguments);\n        if ([\"hr.employee\", \"hr.employee.public\"].includes(this.relation)) {\n            this.avatarCard = usePopover(AvatarCardEmployeePopover, { closeOnClickAway: true });\n        }\n    },\n    get displayAvatarCard() {\n        return (\n            (!this.env.isSmall && [\"hr.employee\", \"hr.employee.public\"].includes(this.relation)) ||\n            super.displayAvatarCard\n        );\n    },\n    getAvatarCardProps() {\n        const originalProps = super.getAvatarCardProps();\n        if ([\"hr.employee\", \"hr.employee.public\"].includes(this.relation)) {\n            return {\n                ...originalProps,\n                recordModel: this.relation,\n            };\n        }\n        return originalProps;\n    },\n};\n\npatch(Many2OneAvatarUserField.prototype, patchMany2OneAvatarUserField);\npatch(KanbanMany2OneAvatarUserField.prototype, patchMany2OneAvatarUserField);\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\n\nimport { formView } from \"@web/views/form/form_view\";\nimport { FormController } from \"@web/views/form/form_controller\";\n\nimport { useArchiveEmployee } from \"@hr/views/archive_employee_hook\";\n\nexport class EmployeeFormController extends FormController {\n    setup() {\n        super.setup();\n        this.archiveEmployee = useArchiveEmployee();\n    }\n\n    getStaticActionMenuItems() {\n        const menuItems = super.getStaticActionMenuItems();\n        menuItems.archive.callback = this.archiveEmployee.bind(this, this.model.root.resId);\n        return menuItems;\n    }\n}\n\nregistry.category(\"views\").add(\"hr_employee_form\", {\n    ...formView,\n    Controller: EmployeeFormController,\n});\n", "import { Component } from \"@odoo/owl\";\n\nexport class HrActionHelper extends Component {\n    static template = \"hr.EmployeeActionHelper\";\n    static props = { noContentTitle: { type: String }, noContentParagraph: { type: String } };\n}\n", "/** @odoo-module */\n\nimport { registry } from '@web/core/registry';\n\nimport { listView } from '@web/views/list/list_view';\nimport { ListController } from '@web/views/list/list_controller';\n\nimport { useArchiveEmployee } from '@hr/views/archive_employee_hook';\n\nexport class EmployeeListController extends ListController {\n    setup() {\n        super.setup();\n        this.archiveEmployee = useArchiveEmployee();\n    }\n\n    getStaticActionMenuItems() {\n        const menuItems = super.getStaticActionMenuItems();\n        const selectedRecords = this.model.root.selection;\n\n        // Only override the Archive action when only 1 record is selected.\n        if (selectedRecords.length === 1 && selectedRecords[0].data.active) {\n            menuItems.archive.callback = this.archiveEmployee.bind(this, selectedRecords[0].resId);\n        }\n        return menuItems;\n    }\n}\n\nregistry.category('views').add('hr_employee_list', {\n    ...listView,\n    Controller: EmployeeListController,\n});\n", "/** @odoo-module **/\n\nimport { helpers } from \"@mail/core/web/open_chat_hook\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(helpers, {\n    SUPPORTED_M2X_AVATAR_MODELS: [\n        ...helpers.SUPPORTED_M2X_AVATAR_MODELS,\n        \"hr.employee\",\n        \"hr.employee.public\",\n    ],\n    buildOpenChatParams(resModel, id) {\n        if ([\"hr.employee\", \"hr.employee.public\"].includes(resModel)) {\n            return { employeeId: id };\n        }\n        return super.buildOpenChatParams(...arguments);\n    }\n});\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { formView } from \"@web/views/form/form_view\";\n\nexport class EmployeeProfileController extends formView.Controller {\n    setup() {\n        super.setup();\n        this.action = useService(\"action\");\n        this.mustReload = false;\n    }\n\n    onWillSaveRecord(record, changes) {\n        this.mustReload = \"lang\" in changes;\n    }\n\n    async onRecordSaved(record) {\n        await super.onRecordSaved(...arguments);\n        if (this.mustReload) {\n            this.mustReload = false;\n            return this.action.doAction(\"reload_context\");\n        }\n    }\n}\n\nregistry.category(\"views\").add(\"hr_employee_profile_form\", {\n    ...formView,\n    Controller: EmployeeProfileController,\n});\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from '@web/core/utils/hooks';\nimport { Dialog } from '@web/core/dialog/dialog';\nimport { SelectMenu } from '@web/core/select_menu/select_menu';\nimport { DropdownItem } from '@web/core/dropdown/dropdown_item';\nimport { user } from \"@web/core/user\";\nimport { Component, useEffect, onWillStart, useRef, useState } from '@odoo/owl';\n\nexport class ArticleSelectionDialog extends Component {\n\n    static template = 'knowledge.ArticleSelectionDialog';\n    static components = { Dialog, DropdownItem, SelectMenu };\n    static props = {\n        articleSelected: Function,\n        close: Function,\n        confirmLabel: String,\n        title: String,\n        parentArticleId: { type: Number, optional: true },\n    };\n\n\n    /**\n     * @override\n     */\n    setup() {\n        super.setup();\n        this.orm = useService('orm');\n        this.placeholderLabel = _t('Choose an Article...');\n        this.toggler = useRef('togglerRef');\n        this.state = useState({\n            selectedArticleName: false,\n            knowledgeArticles: [],\n            createLabel: ''\n        });\n\n        //autofocus\n        useEffect((toggler) => {\n            toggler.click();\n        }, () => [this.toggler.el]);\n\n        onWillStart(async () => {\n            await this.fetchArticles();\n            this.state.isInternalUser = await user.hasGroup('base.group_user');\n        });\n    }\n\n    async createKnowledgeArticle(label) {\n        const articleId = await this.orm.call(\n            'knowledge.article',\n            'article_create',\n            [],\n            {title: label, parent_id: this.props.parentArticleId}\n        );\n        this.props.articleSelected({articleId: articleId, displayName: `\ud83d\udcc4 ${label}`});\n        this.props.close();\n        if (this.props.parentArticleId) {\n            this.env.bus.trigger('knowledge.sidebar.insertNewArticle', {\n                articleId: articleId,\n                name: label,\n                icon: '\ud83d\udcc4',\n                parentId: this.props.parentArticleId,\n            });\n        }\n    }\n\n    async fetchArticles(searchValue) {\n        this.state.createLabel = _t('Create \"%s\"', searchValue);\n        const domain = [\n            ['user_has_access', '=', true],\n            ['is_template', '=', false]\n        ];\n        if (searchValue) {\n            domain.push(['name', '=ilike', `%${searchValue}%`]);\n        }\n        const knowledgeArticles = await this.orm.searchRead(\n            'knowledge.article',\n            domain,\n            ['id', 'display_name', 'root_article_id'], {\n                limit: 20\n            });\n        this.state.knowledgeArticles = knowledgeArticles.map(({ id, display_name, root_article_id }) => {\n            return {\n                value: {\n                    articleId: id,\n                    rootArticleName:  root_article_id[0] !== id ? root_article_id[1] : ''\n                },\n                label: display_name\n            };\n        });\n    }\n\n    async selectArticle(value) {\n        this.selectedArticle = this.state.knowledgeArticles.find(knowledgeArticle => knowledgeArticle.value.articleId === value.articleId);\n        this.state.selectedArticleName = this.selectedArticle.label;\n    }\n\n    confirmArticleSelection() {\n        this.props.articleSelected({articleId: this.selectedArticle.value.articleId, displayName: this.selectedArticle.label});\n        this.props.close();\n    }\n\n}\n", "import { Component, onWillStart, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { groupBy } from \"@web/core/utils/arrays\";\nimport { Record } from \"@web/model/record\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { KnowledgeHtmlViewer } from \"@knowledge/components/knowledge_html_viewer/knowledge_html_viewer\";\nimport { WithSubEnv } from \"@knowledge/components/with_sub_env/with_sub_env\";\nimport { READONLY_MAIN_EMBEDDINGS } from \"@html_editor/others/embedded_components/embedding_sets\";\nimport { KNOWLEDGE_READONLY_EMBEDDINGS } from \"@knowledge/editor/embedded_components/embedding_sets\";\n\n/**\n * This component will display an article template picker. The user will be able\n * to preview the article templates and select the one they want.\n */\nexport class ArticleTemplatePickerDialog extends Component {\n    static template = \"knowledge.ArticleTemplatePickerDialog\";\n    static components = {\n        Dialog,\n        Record,\n        KnowledgeHtmlViewer,\n        WithSubEnv\n    };\n    static props = {\n        onLoadTemplate: { type: Function },\n        close: { type: Function },\n    };\n    /**\n     * @override\n     */\n    setup() {\n        super.setup();\n        this.size = \"fs\";\n        this.orm = useService(\"orm\");\n        this.scrollView = useRef(\"scroll-view\");\n        this.state = useState({});\n\n        onWillStart(async () => {\n            const templates = await this.orm.searchRead(\n                \"knowledge.article\",\n                [\n                    [\"is_template\", \"=\", true],\n                    [\"parent_id\", \"=\", false]\n                ],\n                [\n                    \"id\",\n                    \"icon\",\n                    \"template_name\",\n                    \"template_category_id\",\n                    \"template_category_sequence\",\n                    \"template_sequence\",\n                ],\n                {}\n            );\n            const groups = groupBy(templates, template => template[\"template_category_id\"][0]);\n            this.groups = Object.values(groups).sort((a, b) => {\n                return a[0][\"template_category_sequence\"] > b[0][\"template_category_sequence\"];\n            }).map(group => group.sort((a, b) => {\n                return a[\"template_sequence\"] > b[\"template_sequence\"];\n            }));\n            if (this.groups.length > 0) {\n                this.state.resId = this.groups[0][0].id;\n            }\n        });\n\n        useEffect(() => {\n            const { el } = this.scrollView;\n            if (el) {\n                el.style.visibility = \"visible\";\n            }\n        }, () => [this.state.resId]);\n    }\n\n    /**\n     * @param {integer} articleTemplateId\n     */\n    async onSelectTemplate(articleTemplateId) {\n        const { el } = this.scrollView;\n        el.scrollTop = 0;\n        if (articleTemplateId !== this.state.resId) {\n            el.style.visibility = \"hidden\";\n            this.state.resId = articleTemplateId;\n        }\n    }\n\n    async onLoadTemplate() {\n        this.props.onLoadTemplate(this.state.resId);\n        this.props.close();\n    }\n\n    /**\n     * @param {Record} record\n     * @returns {Object}\n     */\n    getHtmlViewerConfig(record) {\n        return {\n            config: {\n                value: record.data.template_preview,\n                embeddedComponents: [...READONLY_MAIN_EMBEDDINGS, ...KNOWLEDGE_READONLY_EMBEDDINGS],\n            },\n        };\n    }\n\n    /**\n     * @returns {Array[String]}\n     */\n    get articleTemplateFieldNames() {\n        return [\n            \"cover_image_url\",\n            \"icon\",\n            \"id\",\n            \"parent_id\",\n            \"template_name\",\n            \"template_preview\",\n            \"template_description\",\n        ];\n    }\n}\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport default class KnowledgeBreadcrumbs extends Component {\n    static template = \"knowledge.KnowledgeBreadcrumbs\";\n    static props = {\n        record: Object,\n    };\n\n    setup() {\n        super.setup();\n        this.actionService = useService(\"action\");\n        this.articlesIndexes = [this.props.record.resId];\n        this.articleIndex = 0;\n        this.canRestorePreviousAction = this.env.config.breadcrumbs?.length > 1;\n        useRecordObserver((record) => {\n            // When an article is opened, update the array of ids if it was not opened using the\n            // breadcrumbs. For example, if the array of ids is [1,2,3,4] and we are currently on\n            // the article 2 after having clicked twice on the back button, opening article 5\n            // discards the ids after 2 and appends id 5 to the array ([1,2,5])\n            if (record.resId !== this.articlesIndexes[this.articleIndex]) {\n                this.articlesIndexes.splice(\n                    ++this.articleIndex,\n                    this.articlesIndexes.length - this.articleIndex,\n                    record.resId,\n                );\n            }\n        });\n    }\n\n    get isGoBackEnabled() {\n        return this.articleIndex > 0 || this.canRestorePreviousAction;\n    }\n\n    get isGoNextEnabled() {\n        return this.articleIndex < this.articlesIndexes.length - 1;\n    }\n\n    onClickBack() {\n        if (this.isGoBackEnabled) {\n            if (this.articleIndex === 0) {\n                this.actionService.restore();\n            } else {\n                this.env.openArticle(this.articlesIndexes[--this.articleIndex]);\n            }\n        }\n    }\n\n    onClickNext() {\n        if (this.isGoNextEnabled) {\n            this.env.openArticle(this.articlesIndexes[++this.articleIndex]);\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { registry } from '@web/core/registry';\nimport { standardWidgetProps } from '@web/views/widgets/standard_widget_props';\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Chatter } from '@mail/chatter/web_portal/chatter';\nimport { SIZES } from \"@web/core/ui/ui_service\";\n\n\nimport { Component, useRef, useState } from '@odoo/owl';\n\nexport class KnowledgeArticleChatter extends Component {\n    static template = 'knowledge.KnowledgeArticleChatter';\n    static components = { Chatter };\n    static props = { ...standardWidgetProps };\n\n    setup() {\n        this.state = useState({\n            displayChatter: false,\n        });\n\n        this.root = useRef('root')\n        this.ui = useService(\"ui\");\n\n        this.env.bus.addEventListener('KNOWLEDGE:TOGGLE_CHATTER', this.toggleChatter.bind(this));\n    }\n\n    toggleChatter(event) {\n        this.state.displayChatter = event.detail.displayChatter;\n        if (this.state.displayChatter) {\n            this.root.el?.parentElement?.classList.remove('d-none');\n        } else {\n            this.root.el?.parentElement?.classList.add('d-none');\n        }\n    }\n\n    get isChatterAside() {\n        return this.ui.size >= SIZES.LG;\n    }\n}\n\nexport const knowledgeChatterPanel = {\n    component: KnowledgeArticleChatter,\n    additionalClasses: [\n        'o_knowledge_chatter',\n        'col-12',\n        'col-lg-4',\n        'position-relative',\n        'd-none',\n        'p-0',\n        'test'\n    ]\n};\n\nregistry.category('view_widgets').add('knowledge_chatter_panel', knowledgeChatterPanel);\n", "/** @odoo-module **/\n\nimport { CustomFavoriteItem } from \"@web/search/custom_favorite_item/custom_favorite_item\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(CustomFavoriteItem.prototype, {\n    setup() {\n        super.setup(...arguments);\n\n        if (this.isKnowledgeEmbeddedView()) {\n            // knowledge favorites are shared for all users\n            this.state.isShared = true;\n        }\n    },\n\n    isKnowledgeEmbeddedView() {\n        return (\n            this.env.searchModel &&\n            this.env.searchModel.context &&\n            this.env.searchModel.context.knowledgeEmbeddedViewId\n        );\n    },\n});\n", "/** @odoo-module */\n\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { registry } from \"@web/core/registry\";\nimport { Component } from \"@odoo/owl\";\n\nconst cogMenuRegistry = registry.category(\"cogMenu\");\n\nexport class EmbeddedViewActionsMenu extends Component {\n    static props = {};\n    static template = \"knowledge.EmbeddedViewActionsMenu\";\n    static components = { Dropdown, DropdownItem };\n\n    _onOpenEmbeddedView () {\n        this.env.bus.trigger(`KNOWLEDGE_EMBEDDED_${this.env.searchModel.context.knowledgeEmbeddedViewId}:OPEN`);\n    }\n    _onEditEmbeddedView () {\n        this.env.bus.trigger(`KNOWLEDGE_EMBEDDED_${this.env.searchModel.context.knowledgeEmbeddedViewId}:EDIT`);\n    }\n}\n\ncogMenuRegistry.add(\n    'embedded-view-actions-menu',\n    {\n        Component: EmbeddedViewActionsMenu,\n        groupNumber: 10,\n        isDisplayed: (env) => {\n            /**\n             * Those buttons should only be displayed when inside the main Knowledge view.\n             * This means that the context should contain an embedded ID and a context key called\n             * `isOpenedEmbeddedView`. (Which is added when clicking on the open button)\n             */\n            return env.searchModel.context.knowledgeEmbeddedViewId &&\n                    !env.searchModel.context.isOpenedEmbeddedView;\n        },\n    },\n);\n", "import { Component } 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 { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { useOwnedDialogs, useService } from \"@web/core/utils/hooks\";\nimport { omit } from \"@web/core/utils/objects\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { SelectCreateDialog } from \"@web/views/view_dialogs/select_create_dialog\";\n\nconst cogMenuRegistry = registry.category(\"cogMenu\");\n\nconst supportedEmbeddedViews = new Set([\n    \"calendar\",\n    \"graph\",\n    \"hierarchy\",\n    \"kanban\",\n    \"list\",\n    \"pivot\",\n    \"cohort\",\n    \"gantt\",\n    \"map\",\n]);\n\nclass InsertEmbeddedViewMenu extends Component {\n    static props = {};\n    static template = \"knowledge.InsertEmbeddedViewMenu\";\n    static components = { Dropdown, DropdownItem };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.actionService = useService(\"action\");\n        this.addDialog = useOwnedDialogs();\n        this.knowledgeCommandsService = useService(\"knowledgeCommandsService\");\n    }\n\n    /**\n     * @returns {Object|null} Template props necessary to render an embedded\n     *                        view in Knowledge, or null if it is not possible\n     *                        to store this view as an embedded view.\n     */\n    extractCurrentViewEmbedTemplateProps() {\n        /**\n         * Note: If you change the embedded view props (i.e: context key, etc.),\n         * do not forget to update the related python tests.\n         */\n        const viewProps = {\n            context: this.getViewContext(),\n            displayName: this.env.config.getDisplayName(),\n            viewType: this.env.config.viewType,\n        };\n        const xmlId = this.actionService.currentController?.action?.xml_id;\n        if (xmlId) {\n            viewProps.actionXmlId = xmlId;\n            return { embeddedProps: { viewProps } };\n        }\n        /**\n         * Recover the original action (before the service pre-processing). The\n         * raw action is needed because it will be pre-processed again as a\n         * \"different\" action, after being stripped of its id, in Knowledge.\n         * If there is no original action, it means that the action is not\n         * serializable, therefore it cannot be stored in the body of an\n         * article.\n         */\n        const originalAction = this.actionService.currentController?.action?._originalAction;\n        if (originalAction) {\n            const action = JSON.parse(originalAction);\n            // remove action help as it won't be used\n            delete action.help;\n            viewProps.actWindow = action;\n            return { embeddedProps: { viewProps } };\n        }\n        return null;\n    }\n\n    /**\n     * Returns the full context that will be passed to the embedded view.\n     * @returns {Object}\n     */\n    getViewContext() {\n        const context = {};\n        if (this.env.searchModel) {\n            // Store the context of the search model:\n            Object.assign(\n                context,\n                omit(this.env.searchModel.context, ...Object.keys(user.context))\n            );\n            // Store the state of the search model:\n            Object.assign(context, {\n                knowledge_search_model_state: JSON.stringify(this.env.searchModel.exportState()),\n            });\n        }\n        // Store the \"local context\" of the view:\n        const fns = this.env.__getContext__.callbacks;\n        const localContext = Object.assign({}, ...fns.map((fn) => fn()));\n        const extraContext = {};\n        this.env.searchModel.trigger(\"insert-embedded-view\", extraContext);\n        Object.assign(context, localContext, extraContext);\n        return context;\n    }\n\n    /**\n     * Prepare a Embedded Component rendered in backend to be inserted in an\n     * article by the KnowledgeCommandsService.\n     * Allow to choose an article in a modal, redirect to that article and\n     * append the rendered template \"blueprint\" needed for the desired Embedded\n     * Component\n     * @param {string} template template name of the embedded blueprint to\n     *                 render.\n     */\n    insertCurrentViewInKnowledge(template) {\n        const config = this.env.config;\n        const templateProps = this.extractCurrentViewEmbedTemplateProps();\n        if (config.actionType !== \"ir.actions.act_window\" || !templateProps) {\n            throw new Error(\n                'This view can not be embedded in an article: the action is not an \"ir.actions.act_window\" or is not serializable.'\n            );\n        }\n        this.openArticleSelector(async (id) => {\n            this.knowledgeCommandsService.setPendingEmbeddedBlueprint({\n                embeddedBlueprint: renderToElement(template, {\n                    embeddedProps: JSON.stringify(templateProps.embeddedProps),\n                }),\n                model: \"knowledge.article\",\n                field: \"body\",\n                resId: id,\n            });\n            this.actionService.doAction(\"knowledge.ir_actions_server_knowledge_home_page\", {\n                additionalContext: {\n                    res_id: id,\n                },\n            });\n        });\n    }\n\n    onInsertEmbeddedViewInArticle() {\n        this.insertCurrentViewInKnowledge(\"knowledge.EmbeddedViewBlueprint\");\n    }\n\n    onInsertViewLinkInArticle() {\n        this.insertCurrentViewInKnowledge(\"knowledge.EmbeddedViewLinkBlueprint\");\n    }\n\n    /**\n     * @param {Function} onSelectCallback\n     */\n    openArticleSelector(onSelectCallback) {\n        this.addDialog(SelectCreateDialog, {\n            title: _t(\"Select an article\"),\n            noCreate: false,\n            multiSelect: false,\n            resModel: \"knowledge.article\",\n            context: {},\n            domain: [\n                [\"user_has_write_access\", \"=\", true],\n                [\"is_template\", \"=\", false],\n            ],\n            onSelected: (resIds) => {\n                onSelectCallback(resIds[0]);\n            },\n            onCreateEdit: async () => {\n                const articleId = await this.orm.call(\"knowledge.article\", \"article_create\", [], {\n                    is_private: true,\n                });\n                onSelectCallback(articleId);\n            },\n        });\n    }\n}\n\ncogMenuRegistry.add(\"insert-embedded-view-menu\", {\n    Component: InsertEmbeddedViewMenu,\n    groupNumber: 10,\n    isDisplayed: (env) => {\n        // only support act_window with an id for now, but act_window\n        // object could potentially be used too (rework backend API to insert\n        // views in articles)\n        return (\n            env.config.actionId &&\n            !env.searchModel.context.knowledgeEmbeddedViewId &&\n            supportedEmbeddedViews.has(env.config.viewType)\n        );\n    },\n});\n", "import { useBus } from \"@web/core/utils/hooks\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\n\npatch(ListRenderer.prototype, {\n    setup() {\n        super.setup();\n        if (this.env.searchModel) {\n            useBus(this.env.searchModel, \"insert-embedded-view\", (ev) => {\n                Object.assign(ev.detail, {\n                    orderBy: JSON.stringify(this.props.list.orderBy),\n                    keyOptionalFields: this.keyOptionalFields,\n                });\n            });\n        }\n    },\n\n    /**\n     * When the user hides/shows some columns from the list view, the system will\n     * add a new cache entry in the local storage of the user and will list all\n     * visible columns for the current view. To make the configuration specific to\n     * a view, the system generates a unique key for the cache entry by using all\n     * available information about the view.\n     *\n     * When loading the view, the system regenerates a key from the current view\n     * and check if there is any entry in the cache for that key. If there is a\n     * match, the system will load the configuration specified in the cache entry.\n     *\n     * For the embedded views of Knowledge, we want the configuration of the view\n     * to be unique for each embedded view. To achieve that, we will overwrite the\n     * function generating the key for the cache entry and include the unique id\n     * of the embedded view.\n     *\n     * @override\n     * @returns {string}\n     */\n    createViewKey() {\n        if (this.env.searchModel?.context.knowledgeEmbeddedViewId) {\n            return `${super.createViewKey()},${this.env.searchModel.context.knowledgeEmbeddedViewId}`;\n        }\n        return super.createViewKey();\n    },\n});\n", "/** @odoo-module **/\n\nimport { FormStatusIndicator } from \"@web/views/form/form_status_indicator/form_status_indicator\";\n\n/**\n * This extension of the FormStatusIndicator is used to add a new indicator to the ones that already\n * exists. This new icon is used in the same way as the icon in Google Docs => indicate that all changes\n * have been committed to the DB.\n */\nexport class KnowledgeFormStatusIndicator extends FormStatusIndicator {\n    static template = 'knowledge.FormStatusIndicator';\n}\n", "import { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\n\nimport KnowledgeBreadcrumbs from \"@knowledge/components/breadcrumbs/breadcrumbs\";\nimport KnowledgeIcon from \"@knowledge/components/knowledge_icon/knowledge_icon\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nexport default class KnowledgeHierarchy extends Component {\n    static components = {\n        Dropdown,\n        DropdownItem,\n        KnowledgeBreadcrumbs,\n        KnowledgeIcon,\n    };\n    static props = { record: Object };\n    static template = \"knowledge.KnowledgeHierarchy\";\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.state = useState({\n            articleName: this.props.record.data.name,\n            isLoadingArticleHierarchy: false,\n        });\n        useRecordObserver((record) => {\n            if (this.state.articleName !== record.data.name) {\n                this.state.articleName = record.data.name;\n            }\n        });\n    }\n\n    /**\n     * Whether to display the dropdown toggle used to get the articles that are between the root\n     * and the parent article. It is only shown if there are any articles to show (parent_path is\n     * of the form \"1/2/3/4/\", hence length > 4 as condition)\n     */\n    get displayDropdownToggle() {\n        return this.props.record.data.parent_path.split(\"/\").length > 4;\n    }\n\n    get isReadonly() {\n        return this.props.record.data.is_locked || !this.props.record.data.user_can_write;\n    }\n\n    get parentId() {\n        return this.props.record.data.parent_id?.[0];\n    }\n\n    get parentName() {\n        return this.props.record.data.parent_id?.[1];\n    }\n\n    get rootId() {\n        return this.props.record.data.root_article_id[0];\n    }\n\n    get rootName() {\n        return this.props.record.data.root_article_id[1];\n    }\n\n    /**\n     * Load the articles that should be shown in the dropdown\n     */\n    async loadHierarchy() {\n        this.articleHierarchy = await this.orm.call(\n            \"knowledge.article\",\n            \"get_article_hierarchy\",\n            [this.props.record.resId],\n            { exclude_article_ids: [this.rootId, this.parentId, this.props.record.resId] },\n        );\n        this.state.isLoadingArticleHierarchy = false;\n    }\n\n    /**\n     * If needed, will show the loading indicator in the dropdown and start the loading\n     * of the articles to show in it\n     */\n    async onBeforeOpen() {\n        this.state.isLoadingArticleHierarchy = true;\n        this.loadHierarchy();\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { SCALE_LABELS } from \"@web/views/calendar/calendar_controller\";\nimport { SelectMenu } from \"@web/core/select_menu/select_menu\";\nimport { useAutofocus, useService } from \"@web/core/utils/hooks\";\nimport { uuid } from \"@web/views/utils\";\n\nimport { Component, onWillStart, useExternalListener, useState } from \"@odoo/owl\";\n\n/**\n * Dialog allowing to dynamically edit the item calendar view configuration\n * (the \"itemCalendarProps\"). Used when clicking on the \"edit\" button of the\n * embedded view manager.\n */\nexport class ItemCalendarPropsDialog extends Component {\n    static template = \"knowledge.ItemCalendarPropsDialog\";\n    static components = {\n        DropdownItem,\n        Dialog,\n        SelectMenu,\n    };\n    static props = {\n        knowledgeArticleId: { type: Number, optional: true },\n        close: { type: Function, optional: true },\n        colorPropertyId: { type: String, optional: true },\n        dateStartPropertyId: { type: String, optional: true },\n        dateStopPropertyId: { type: String, optional: true },\n        dateType: { type: String, optional: true },\n        isNew: { type: Boolean, optional: true },\n        name: { type: String, optional: true },\n        saveItemCalendarProps: { type: Function },\n        scale: { type: String, optional: true },\n        showWeekEnds: { type: Boolean, optional: true },\n        slotMaxTime: { type: String, optional: true },\n        slotMinTime: { type: String, optional: true },\n    };\n\n    static defaultProps = {\n        showWeekEnds: true,\n        slotMinTime: \"00:00\",\n        slotMaxTime: \"24:00\",\n    };\n\n    setup() {\n        super.setup();\n        useAutofocus();\n        this.orm = useService(\"orm\");\n        this.notification = useService(\"notification\");\n        this.state = useState({\n            name: this.props.name,\n            scale: this.props.scale || \"week\",\n            showWeekEnds: this.props.showWeekEnds,\n            slotMaxTime: this.props.slotMaxTime,\n            slotMinTime: this.props.slotMinTime,\n        });\n        this.colorChoices = [];\n        this.dateChoices = [];\n        this.dateTimeChoices = [];\n        this.propertyFieldEntries = {};\n        this.scalesChoices = Object.entries(SCALE_LABELS).map(([value, label]) => ({ value, label }));\n\n        onWillStart(async () => {\n            // Fetch the properties definitions and create choices to use in\n            // the SelectMenu components\n            const propertiesDefinitions = await this.orm.read(\n                \"knowledge.article\",\n                [this.props.knowledgeArticleId],\n                [\"article_properties_definition\"]\n            );\n            this.propertiesDefinitions = propertiesDefinitions[0].article_properties_definition;\n            for (const definition of this.propertiesDefinitions) {\n                this.propertyFieldEntries[definition.name] = {\n                    label: definition.string,\n                    type: definition.type,\n                };\n                // Add choice in corresponding dropdown\n                const choice = {\n                    value: definition.name,\n                    label: definition.string,\n                };\n                if (definition.type === \"datetime\") {\n                    this.dateTimeChoices.push(choice);\n                } else if (definition.type === \"date\") {\n                    this.dateChoices.push(choice);\n                } else if ([\"boolean\", \"many2one\", \"selection\"].includes(definition.type)) {\n                    this.colorChoices.push(choice);\n                }\n            }\n\n            if (this.props.isNew) {\n                // If no date(time) properties exists, create default ones\n                if (!this.dateTimeChoices.length && !this.dateChoices.length) {\n                    this.autoCreateDateProperties = true;\n                    this.createProperty(_t(\"Start Date Time\"), \"datetime\", \"dateStart\");\n                    this.createProperty(_t(\"End Date Time\"), \"datetime\", \"dateStop\");\n                } else {\n                    // If some exist, select them by default (prefer to use 2\n                    // of the same type if possible, and prefer datetimes over\n                    // dates)\n                    if (this.dateTimeChoices.length > 1) {\n                        this.selectDateStart(this.dateTimeChoices[0].value);\n                        this.selectDateStop(this.dateTimeChoices[1].value);\n                    } else if (this.dateChoices.length > 1) {\n                        this.selectDateStart(this.dateChoices[0].value);\n                        this.selectDateStop(this.dateChoices[1].value);\n                    } else if (this.dateTimeChoices.length !== 0) {\n                        this.selectDateStart(this.dateTimeChoices[0].value);\n                    } else {\n                        this.selectDateStart(this.dateChoices[0].value);\n                    }\n\n                }\n            } else {\n                // Use props only if the related property still exists\n                for (const propName of [\"dateStartPropertyId\", \"dateStopPropertyId\", \"colorPropertyId\"]) {\n                    if (this.propertyFieldEntries[this.props[propName]]) {\n                        this.state[propName] = this.props[propName];\n                    }\n                }\n            }\n        });\n\n        // Save when pressing on enter\n        useExternalListener(window, \"keydown\", (event) => {\n            if (event.key === \"Enter\") {\n                this.save();\n            }\n        });\n    }\n\n    /**\n     * Return the available start properties formatted as groups (date and\n     * datetime) for the SelectMenu component\n     */\n    get availableDateStartProperties() {\n        return [{\n            label: _t(\"Date and Time Properties\"),\n            choices: this.dateTimeChoices\n        }, {\n            label: _t(\"Date Properties\"),\n            choices: this.dateChoices,\n        }];\n    }\n\n    /**\n     * Return the available stop properties (that are of the same type as the\n     * current start date) formatted as a group for the SelectMenu component\n     */\n    get availableDateStopProperties() {\n        // Don't show current start date nor dates with other type\n        if (this.dateStartProperty?.type === \"datetime\") {\n            return [{\n                label: _t(\"Date and Time Properties\"),\n                choices: this.dateTimeChoices.filter(choice => choice.value !== this.state.dateStartPropertyId),\n            }];\n        }\n        return [{\n            label: _t(\"Date Properties\"),\n            choices: this.dateChoices.filter(choice => choice.value !== this.state.dateStartPropertyId),\n        }];\n    }\n\n    get colorProperty() {\n        return this.propertyFieldEntries[this.state.colorPropertyId];\n    }\n\n    get dateStartProperty() {\n        return this.propertyFieldEntries[this.state.dateStartPropertyId];\n    }\n\n    get dateStopProperty() {\n        return this.propertyFieldEntries[this.state.dateStopPropertyId];\n    }\n\n    /**\n     * Create a new date or datetime property and use it as start or stop\n     * choice.\n     * Note: only the new properties that are selected when saving will be\n     * stored.\n     * @param {string} label: label of the property\n     * @param {string} type: type of the property (date or datetime)\n     * @param {string} calendarProp: for which calendar prop the property has\n     * been created\n     */\n    createProperty(label, type, calendarProp) {\n        const newPropertyId = uuid();\n        // Add to list of properties\n        this.propertyFieldEntries[newPropertyId] = {\n            isNew: true,\n            label: label,\n            type: type,\n        };\n        // Add new choice in dropdowns\n        const newChoice = {\n            value: newPropertyId,\n            label,\n        };\n        if (type === \"date\") {\n            this.dateChoices.push(newChoice);\n        } else if (type === \"datetime\") {\n            this.dateTimeChoices.push(newChoice);\n        } else {\n            this.colorChoices.push(newChoice);\n        }\n        // Select the new choice\n        if (calendarProp === \"dateStart\") {\n            this.selectDateStart(newPropertyId);\n        } else if (calendarProp === \"dateStop\") {\n            this.selectDateStop(newPropertyId);\n        } else if (calendarProp === \"color\") {\n            this.selectColor(newPropertyId);\n        }\n    }\n\n    /**\n     * Save the item calendar props and close the dialog. If there is a new\n     * choice selected as start and/or stop date, create the associated\n     * property(ies) first.\n     */\n    async save() {\n        if (!this.state.dateStartPropertyId) {\n            this.notification.add(_t(\"The start date property is required.\"), {type: \"danger\"});\n            return;\n        }\n        // Create new property if needed\n        if (this.dateStartProperty.isNew || this.dateStopProperty?.isNew || this.colorProperty?.isNew) {\n            // Keep existing properties to not lose them.\n            const propertiesDefinitions = [...this.propertiesDefinitions];\n            if (this.dateStartProperty.isNew) {\n                propertiesDefinitions.push({\n                    name: this.state.dateStartPropertyId,\n                    string: this.dateStartProperty.label,\n                    type: this.dateStartProperty.type,\n                });\n            }\n            if (this.dateStopProperty?.isNew) {\n                propertiesDefinitions.push({\n                    name: this.state.dateStopPropertyId,\n                    string: this.dateStopProperty.label,\n                    type: this.dateStopProperty.type,\n                });\n            }\n            if (this.colorProperty?.isNew) {\n                propertiesDefinitions.push({\n                    name: this.state.colorPropertyId,\n                    string: this.colorProperty.label,\n                    type: this.colorProperty.type,\n                });\n            }\n            try {\n                await this.orm.write(\"knowledge.article\", [this.props.knowledgeArticleId], {article_properties_definition: propertiesDefinitions});\n            } catch (e) {\n                this.notification.add(_t(\"New property could not be created.\"), {type: \"danger\"});\n                console.error(e);\n                return;\n            }\n        }\n        this.props.saveItemCalendarProps(this.state.name, {\n            dateStartPropertyId: this.state.dateStartPropertyId,\n            dateStopPropertyId: this.state.dateStopPropertyId || undefined,\n            colorPropertyId: this.state.colorPropertyId || undefined,\n            scale: this.state.scale,\n            dateType: this.dateStartProperty.type,\n            showWeekEnds: this.state.showWeekEnds,\n            slotMinTime: this.state.slotMinTime || \"00:00\",\n            slotMaxTime: this.state.slotMaxTime || \"24:00\",\n        });\n        this.props.close();\n    }\n\n    selectColor(value) {\n        this.state.colorPropertyId = value;\n    }\n\n    selectDateStop(value) {\n        this.state.dateStopPropertyId = value;\n    }\n\n    selectScale(value) {\n        this.state.scale = value;\n    }\n\n    selectDateStart(value) {\n        this.state.dateStartPropertyId = value;\n        // DateStop must be of the same type as start, but must not be the same property\n        if (this.state.dateStopPropertyId && (this.dateStartProperty.type !== this.dateStopProperty.type || this.state.dateStopPropertyId === this.state.dateStartPropertyId)) {\n            this.state.dateStopPropertyId = false;\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { user } from \"@web/core/user\";\nimport { throttleForAnimation } from \"@web/core/utils/timing\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\nimport { Component, onWillStart, useRef, useState } from \"@odoo/owl\";\n\nclass KnowledgeCover extends Component {\n    static props = standardWidgetProps;\n    static template = \"knowledge.KnowledgeCover\";\n\n    setup() {\n        super.setup();\n        this.root = useRef(\"root\");\n        this.image = useRef(\"image\");\n        this.state = useState({\n            repositioning: false,\n            grabbing: false,\n            // cover_image_position is false if the cover has never been repositioned before\n            verticalPosition: this.props.record.data.cover_image_position || 50,\n        });\n\n        onWillStart(async () => {\n            this.isInternalUser = await user.hasGroup('base.group_user');\n        });\n\n        // Update the state when we open an article (because since we open an\n        // article by loading the related record, the state of this component\n        // is shared between the articles and is not recomputed).\n        let previousArticleId = this.props.record.resId;\n        useRecordObserver((record) => {\n            if (!previousArticleId || previousArticleId !== record.resId) {\n                previousArticleId = record.resId;\n                this.state.repositioning = false;\n                this.state.grabbing = false;\n                this.state.verticalPosition = record.data.cover_image_position || 50;\n            }\n        });\n    }\n\n    /**\n     * @returns {boolean} - True if the article is editable\n     */\n    get isEditable() {\n        const recordData = this.props.record.data;\n        return !recordData.is_locked\n            && recordData.user_can_write\n            && recordData.active\n            && this.isInternalUser;\n    }\n\n    changeCover() {\n        this.env.ensureArticleName();\n        this.env.openCoverSelector();\n    }\n\n    removeCover() {\n        this.props.record.update({cover_image_id: false});\n    }\n\n    /**\n     * Make the cover draggable so that the user can reposition it.\n     */\n    repositionCover() {\n        this.state.repositioning = true;\n        const cover = this.image.el;\n        let prevPos;\n\n        const moveCover = throttleForAnimation(ev => {\n            if (prevPos !== ev.y) {\n                // Add offset proportional to the distance traveled by the cursor.\n                // (dividing by 5 to not move the cover too fast).\n                const verticalPosition = this.state.verticalPosition + (prevPos - ev.y) / 5;\n                // Make sure we are between 0.01 and 100% (not 0 since it's the\n                // default value when the cover has never been repositioned).\n                this.state.verticalPosition = Math.max(0.01, Math.min(100, verticalPosition));\n                prevPos = ev.y;\n            }\n        });\n\n        const onPointerUp = () => {\n            this.state.grabbing = false;\n            document.removeEventListener('pointermove', moveCover);\n        };\n\n        const onPointerDown = (ev) => {\n            ev.preventDefault();\n            ev.stopPropagation();\n            prevPos = ev.y;\n            this.state.grabbing = true;\n            document.addEventListener('pointermove', moveCover);\n            document.addEventListener('pointerup', onPointerUp, {once: true});\n        };\n\n        cover.addEventListener('pointerdown', onPointerDown);\n\n        // Click outside the cover (or on save button) leaves the repositioning mode\n        document.addEventListener('pointerdown', (ev) => {\n            cover.removeEventListener('pointerdown', onPointerDown);\n            this.state.repositioning = false;\n            if (ev.target.classList.contains('o_knowledge_undo_cover_move')) {\n                // Undo move\n                this.state.verticalPosition = this.props.record.data.cover_image_position || 50;\n            } else {\n                // Save new position\n                this.props.record.update({cover_image_position: this.state.verticalPosition});\n            }\n        }, {once: true});\n    }\n\n}\n\nexport const knowledgeCover = {\n    component: KnowledgeCover,\n};\nregistry.category(\"view_widgets\").add(\"knowledge_cover\", knowledgeCover);\n", "import { _t } from \"@web/core/l10n/translation\";\nimport {\n    AutoResizeImage,\n    ImageSelector,\n} from \"@html_editor/main/media/media_dialog/image_selector\";\nimport { ConfirmationDialog } from '@web/core/confirmation_dialog/confirmation_dialog';\nimport { Dialog } from '@web/core/dialog/dialog';\nimport { UnsplashError } from '@web_unsplash/unsplash_error/unsplash_error';\nimport { useService, useChildRef } from \"@web/core/utils/hooks\";\nimport { Component } from \"@odoo/owl\";\n\nexport class AutoResizeCover extends AutoResizeImage {\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n    }\n\n    /**\n     * @override\n     * Open Dialog to delete the cover record.\n     */\n    remove() {\n        this.dialogs.add(ConfirmationDialog, {\n            body: _t(\"Are you sure you want to delete this cover? It will be removed from every article it is used in.\"),\n            confirm: async () => {\n                const res = await this.orm.unlink(this.props.model,\n                    [this.props.resId],\n                );\n                if (res) {\n                    this.props.onRemoved(this.props.id, this.props.resId);\n                }\n            },\n        });\n    }\n}\n\nexport class KnowledgeCoverSelector extends ImageSelector {\n    static defaultProps = {\n        resModel: \"knowledge.cover\",\n        orientation: \"landscape\",\n    };\n    static attachmentsListTemplate = \"knowledge.CoversListTemplate\";\n    static components = {\n        ...ImageSelector.components,\n        AutoResizeCover,\n        UnsplashError,\n    };\n    setup() {\n        super.setup();\n        // Search for images matching the article name when opening the dialog.\n        this.unsplashService = useService(\"unsplash\");\n        this.state.needle = this.props.searchTerm;\n        this.searchUnsplash(this.state.needle);\n    }\n\n    /**\n     * @override\n     * Domain used to fetch the attachments to display in the coverSelector.\n     */\n    get attachmentsDomain() {\n        return ['&', ['res_model', '=', this.props.resModel], ['name', 'ilike', this.state.needle]];\n    }\n\n    /**\n     * @override\n     * Update the article's cover using the id of the cover associated to the\n     * attachment clicked.\n     */\n    onClickAttachment(attachment) {\n        this.props.save(attachment.res_id);\n    }\n\n    /**\n     * @override\n     * Upload the unsplash image clicked.\n     */\n    onClickRecord(unsplashRecord) {\n        this.unsplashService.uploadUnsplashRecords(\n            [unsplashRecord],\n            {resModel: this.props.resModel, resId: this.props.articleCoverId},\n            async (attachments) => this.onUploaded(attachments[0])\n        );\n    }\n\n    /**\n     * @override\n     * Remove cover from the opened CoverSelector, and update article's cover\n     * if the cover was used in the current article.\n     */\n    onRemoved(attachmentId, coverId) {\n        super.onRemoved(attachmentId);\n        if (coverId === this.props.articleCoverId) {\n            this.props.save(false);\n        }\n    }\n\n    /**\n     * @override\n     * Associate the created attachment to a new cover record, update the\n     * attachment's resId, and update the article's cover.\n     */\n    async onUploaded(attachment) {\n        const [coverId] = await this.orm.create(this.props.resModel, [{attachment_id: attachment.id}]);\n        this.props.save(coverId);\n    }\n\n    /**\n     * @override\n     * Overridden to not throw an error.\n     * Designed to be used with several tabs in the dialog (not the case here)\n     * and used for rendering purposes to show selected images when multiImage\n     * is allowed (not the case either).\n     */\n    get selectedAttachmentIds() {\n        return [];\n    }\n\n    /**\n     * @override\n     * Overridden to not throw an error.\n     * Designed to be used with several tabs in the dialog (not the case here)\n     * and used for rendering purposes to show selected images when multiImage\n     * is allowed (not the case either).\n     */\n    get selectedRecordIds() {\n        return [];\n    }\n}\n\nexport class KnowledgeCoverDialog extends Component {\n    static template = 'knowledge.KnowledgeCoverDialog';\n    static components = {\n        KnowledgeCoverSelector,\n        Dialog\n    };\n    static props = {\n        articleCoverId: {type: Number, optional: true},\n        articleName: String,\n        save: Function,\n        close: Function,\n    };\n\n    setup() {\n        this.size = 'xl';\n        this.contentClass = 'o_select_media_dialog h-100';\n        this.modalRef = useChildRef();\n    }\n\n    /**\n     * Update the article's cover.\n     */\n    save(coverId) {\n        this.props.save(coverId);\n        this.props.close();\n    }\n}\n", "/** @odoo-module **/\n\nimport { patch } from \"@web/core/utils/patch\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\n\npatch(Dropdown.prototype, {\n    /**\n     * Embedded views in knowledge can become the active element,\n     * but dropdowns should be able to be closed by clicking.\n     * This override is necessary because dropdowns mounted inside an\n     * embedded view register the document as their active element instead\n     * of the embedded view itself.\n     *\n     * @override\n     */\n    popoverCloseOnClickAway(target, activeEl) {\n        const currentActiveEl = this.uiService.getActiveElementOf(target);\n        return (\n            super.popoverCloseOnClickAway(target, activeEl) ||\n            (currentActiveEl !== activeEl &&\n                activeEl &&\n                activeEl.contains(currentActiveEl) &&\n                currentActiveEl.dataset.embedded)\n        );\n    },\n});\n", "import { HtmlField, htmlField } from \"@html_editor/fields/html_field\";\nimport {\n    KNOWLEDGE_EMBEDDINGS,\n    KNOWLEDGE_READONLY_EMBEDDINGS,\n} from \"@knowledge/editor/embedded_components/embedding_sets\";\nimport {\n    KNOWLEDGE_EMBEDDED_COMPONENT_PLUGINS,\n    KNOWLEDGE_PLUGINS,\n} from \"@knowledge/editor/plugin_sets\";\nimport { registry } from \"@web/core/registry\";\nimport { useState, useSubEnv } from \"@odoo/owl\";\nimport { KnowledgeHtmlViewer } from \"../knowledge_html_viewer/knowledge_html_viewer\";\nimport { KnowledgeWysiwyg } from \"../knowledge_wysiwyg/knowledge_wysiwyg\";\nimport { CallbackRecorder } from \"@web/search/action_hook\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class KnowledgeHtmlField extends HtmlField {\n    static components = {\n        ...HtmlField.components,\n        Wysiwyg: KnowledgeWysiwyg,\n        HtmlViewer: KnowledgeHtmlViewer,\n    };\n    setup() {\n        super.setup();\n        useSubEnv({\n            __onLayoutGeometryChange__: new CallbackRecorder(),\n        });\n        this.commentsService = useService(\"knowledge.comments\");\n        this.commentsState = useState(this.commentsService.getCommentsState());\n        useRecordObserver((record) => {\n            if (record.resId !== this.commentsState.articleId) {\n                this.commentsService.setArticleId(record.resId);\n                this.commentsService.loadRecords(record.resId, {\n                    ignoreBatch: true,\n                    includeLoaded: true,\n                });\n            }\n        });\n    }\n\n    getConfig() {\n        const config = super.getConfig();\n        // TODO @engagement: fill this array with knowledge components\n        if (this.props.embeddedComponents) {\n            config.resources.embedded_components = [\n                ...(config.resources.embedded_components || []),\n                ...KNOWLEDGE_EMBEDDINGS,\n            ];\n            // Replace the file plugin with the embedded file plugin\n            config.Plugins = config.Plugins.filter((P) => P.id !== \"file\");\n            config.Plugins.push(...KNOWLEDGE_EMBEDDED_COMPONENT_PLUGINS);\n        }\n        config.Plugins.push(...KNOWLEDGE_PLUGINS);\n        config.onLayoutGeometryChange = () => this.onLayoutGeometryChange();\n        return config;\n    }\n\n    getReadonlyConfig() {\n        const config = super.getReadonlyConfig();\n        if (this.props.embeddedComponents) {\n            config.embeddedComponents = [\n                ...(config.embeddedComponents || []),\n                ...KNOWLEDGE_READONLY_EMBEDDINGS,\n            ];\n        }\n        config.onLayoutGeometryChange = () => this.onLayoutGeometryChange();\n        return config;\n    }\n\n    onLayoutGeometryChange() {\n        for (const cb of this.env.__onLayoutGeometryChange__.callbacks) {\n            cb();\n        }\n    }\n}\n\nexport const knowledgeHtmlField = {\n    ...htmlField,\n    component: KnowledgeHtmlField,\n};\n\nregistry.category(\"fields\").add(\"knowledge_html\", knowledgeHtmlField);\n", "import { HtmlViewer } from \"@html_editor/fields/html_viewer\";\nimport { LocalOverlayContainer } from \"@html_editor/local_overlay_container\";\nimport { usePositionHook } from \"@html_editor/position_hook\";\nimport { onWillDestroy, useEffect, useExternalListener, useState, useSubEnv } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { KnowledgeCommentsHandler } from \"@knowledge/comments/comments_handler/comments_handler\";\nimport { CommentBeaconManager } from \"@knowledge/comments/comment_beacon_manager\";\nimport { useChildRef, useService } from \"@web/core/utils/hooks\";\nimport { uniqueId } from \"@web/core/utils/functions\";\nimport { effect } from \"@web/core/utils/reactive\";\nimport { batched } from \"@web/core/utils/timing\";\n\nexport class KnowledgeHtmlViewer extends HtmlViewer {\n    static template = \"knowledge.KnowledgeHtmlViewer\";\n    static components = {\n        ...HtmlViewer.components,\n        LocalOverlayContainer,\n    };\n\n    setup() {\n        super.setup();\n        this.commentsService = useService(\"knowledge.comments\");\n        this.commentsState = useState(this.commentsService.getCommentsState());\n        let editorThreadsKeys;\n        this.alive = true;\n        useExternalListener(window, \"click\", this.onWindowClick);\n        effect(\n            batched((state) => {\n                if (!this.alive) {\n                    return;\n                }\n                const editorThreads = state.editorThreads;\n                const keys = Object.keys(editorThreads).toString();\n                if (keys !== editorThreadsKeys) {\n                    this.commentBeaconManager?.sortThreads();\n                    this.commentBeaconManager?.drawThreadOverlays();\n                    editorThreadsKeys = keys;\n                }\n            }),\n            [this.commentsState]\n        );\n        onWillDestroy(() => {\n            this.alive = false;\n        });\n        this.overlayRef = useChildRef();\n        useSubEnv({\n            localOverlayContainerKey: uniqueId(\"html_viewer\"),\n        });\n        usePositionHook(this.readonlyElementRef, document, () => {\n            this.commentBeaconManager?.drawThreadOverlays();\n            this.props.config.onLayoutGeometryChange?.();\n        });\n        this.overlayComponentsKey = uniqueId(\"KnowledgeCommentsHandler\");\n        useEffect(\n            // TODO ABD: investigate if we need useRecordObserver if user_permission changes ?\n            // maybe patch occurs too late when user_permission change ?\n            // is it even necessary to check for permission = \"none\" ?\n            () => {\n                let overlayContainer;\n                if (\n                    this.readonlyElementRef.el &&\n                    this.overlayRef.el &&\n                    this.env.model.root.data.user_permission &&\n                    this.env.model.root.data.user_permission !== \"none\"\n                ) {\n                    overlayContainer = this.makeLocalOverlay();\n                    this.overlayRef.el.append(overlayContainer);\n                    // reactive on this object if necessary\n                    this.commentBeaconManager = new CommentBeaconManager({\n                        document,\n                        source: this.readonlyElementRef.el,\n                        overlayContainer: overlayContainer,\n                        commentsState: this.commentsState,\n                        readonly: true,\n                    });\n                    this.commentBeaconManager.sortThreads();\n                    this.commentBeaconManager.drawThreadOverlays();\n                    registry\n                        .category(this.env.localOverlayContainerKey)\n                        .add(this.overlayComponentsKey, {\n                            Component: KnowledgeCommentsHandler,\n                            props: {\n                                commentBeaconManager: this.commentBeaconManager,\n                                contentRef: this.readonlyElementRef,\n                            },\n                        });\n                }\n                return () => {\n                    overlayContainer?.remove();\n                    this.commentBeaconManager?.destroy();\n                    this.commentBeaconManager = undefined;\n                    registry\n                        .category(this.env.localOverlayContainerKey)\n                        .remove(this.overlayComponentsKey);\n                };\n            },\n            () => [this.readonlyElementRef.el, this.overlayRef.el]\n        );\n    }\n\n    onWindowClick(ev) {\n        const selector = \".oe-local-overlay, .o_knowledge_comment_box\";\n        const closestElement = ev.target.closest(selector);\n        if (!closestElement) {\n            this.commentsState.activeThreadId = undefined;\n        }\n    }\n\n    makeLocalOverlay() {\n        const overlayContainer = document.createElement(\"div\");\n        overlayContainer.className = `oe-local-overlay`;\n        overlayContainer.setAttribute(\"data-oe-local-overlay-id\", \"KnowledgeThreadBeacons\");\n        return overlayContainer;\n    }\n}\n", "/** @odoo-module */\n\nimport { useEmojiPicker } from \"@web/core/emoji_picker/emoji_picker\";\nimport { registry } from \"@web/core/registry\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nimport { getRandomIcon } from \"@knowledge/js/knowledge_utils\";\n\nimport { Component, useRef } from \"@odoo/owl\";\n\nexport default class KnowledgeIcon extends Component {\n    static template = \"knowledge.KnowledgeIcon\";\n    static props = {\n        record: Object,\n        readonly: Boolean,\n        iconClasses: {type: String, optional: true},\n        allowRandomIconSelection: {type: Boolean, optional: true},\n        autoSave: {type: Boolean, optional: true},\n        fallbackDefaultIcon: { type: Boolean, optional: true },\n    };\n\n    setup() {\n        super.setup();\n        this.iconRef = useRef(\"icon\");\n        this.emojiPicker = useEmojiPicker(this.iconRef, { hasRemoveFeature: true, onSelect: this.updateIcon.bind(this) });\n    }\n\n    get icon() {\n        return this.props.record.data.icon || (this.props.fallbackDefaultIcon && \"\ud83d\udcc4\");\n    }\n\n    async selectRandomIcon() {\n        this.updateIcon(await getRandomIcon());\n    }\n\n    updateIcon(icon) {\n        this.props.record.update({icon});\n        if (this.props.autoSave) {\n            this.props.record.save();\n        }\n    }\n}\n\nclass KnowledgeIconField extends KnowledgeIcon {\n    static props = {\n        ...standardFieldProps,\n        allowRandomIconSelection: Boolean,\n        autoSave: Boolean,\n    };\n}\n\nregistry.category(\"fields\").add(\"knowledge_icon\", {\n    component: KnowledgeIconField,\n    extractProps({ attrs, viewType }, dynamicInfo) {\n        return {\n            autoSave: viewType === \"kanban\",\n            readonly: dynamicInfo.readonly,\n            allowRandomIconSelection: exprToBoolean(attrs.allow_random_icon_selection),\n        };\n    },\n});\n", "import { Wysiwyg } from \"@html_editor/wysiwyg\";\nimport { isEmptyBlock } from \"@html_editor/utils/dom_info\";\nimport { WysiwygArticleHelper } from \"@knowledge/components/wysiwyg_article_helper/wysiwyg_article_helper\";\nimport { useState } from \"@odoo/owl\";\n\nexport class KnowledgeWysiwyg extends Wysiwyg {\n    static template = \"knowledge.KnowledgeWysiwyg\";\n    static components = {\n        ...Wysiwyg.components,\n        WysiwygArticleHelper,\n    };\n\n    setup() {\n        super.setup();\n        this.articleHelperState = useState({\n            isVisible: false,\n        });\n    }\n\n    /** @override */\n    getEditorConfig() {\n        const config = super.getEditorConfig();\n        return {\n            ...config,\n            onChange: () => {\n                this.articleHelperState.isVisible = isEmptyBlock(this.editor.editable);\n                config.onChange?.();\n            },\n            onEditorReady: () => {\n                this.articleHelperState.isVisible = isEmptyBlock(this.editor.editable);\n            },\n        };\n    }\n}\n", "import { Dialog } from '@web/core/dialog/dialog';\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { SelectMenu } from '@web/core/select_menu/select_menu';\n\nimport { Component, onWillStart, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nclass MoveArticleDialog extends Component {\n\n    static template = \"knowledge.MoveArticleDialog\";\n    static components = { Dialog, SelectMenu };\n    static props = {\n        close: Function,\n        knowledgeArticleRecord: Object\n    };\n\n    setup() {\n        this.size = 'md';\n        this.orm = useService(\"orm\");\n        this.state = useState({selectedParentArticle: false, selectionDisplayGroups: []});\n        this.placeholderLabel = _t('Choose an Article...');\n        this.toggler = useRef(\"togglerRef\");\n\n        onWillStart(this.fetchValues);\n\n        //autofocus\n        useEffect((toggler) => {\n            toggler.click();\n        }, () => [this.toggler.el]);\n\n    }\n\n    /**\n     * For this dialog we needed to get valid articles and the sections since the user can move an\n     * article either under a specific article, that becomes its parent, or inside a section, to become a\n     * root article in either the workspace or the private section.\n     *\n     * @param {String} searchValue Term inputted by the user in the SelectMenu's input\n     */\n    async fetchValues(searchValue) {\n        const knowledgeArticles = await this.orm.call(\n            'knowledge.article',\n            'get_valid_parent_options',\n            [this.props.knowledgeArticleRecord.resId],\n            {search_term: searchValue}\n        );\n        const formattedKnowledgeArticles = knowledgeArticles.map(({id, display_name, root_article_id}) => {\n            return {\n                value: {\n                    parentArticleId: id,\n                    rootArticleName:  root_article_id[0] !== id ? root_article_id[1] : ''\n                },\n                label: display_name\n            };\n        });\n        const selectionGroups = [\n            {\n                label: _t('Categories'),\n                choices: [\n                    {\n                        value: {parentArticleId: 'private'},\n                        label: _t('Private'),\n                    },\n                    {\n                        value: {parentArticleId: 'workspace'},\n                        label: _t('Workspace')\n                    }\n                ],\n            },\n            {\n                label: _t('Articles'),\n                choices: formattedKnowledgeArticles,\n            }\n        ];\n        if (this.props.knowledgeArticleRecord.data.category !== 'private') {\n            selectionGroups[0].choices.push({\n                value: {parentArticleId: 'shared'},\n                label: _t('Shared'),\n            });\n        }\n        this.state.selectionDisplayGroups = selectionGroups;\n        this.state.choices = [\n            ...selectionGroups[0].choices,\n            ...formattedKnowledgeArticles\n        ];\n    }\n\n    selectArticle(value) {\n        this.state.selectedParentArticle = this.state.choices.find(\n            (knowledgeArticle) => knowledgeArticle.value.parentArticleId === value.parentArticleId\n        );\n    }\n\n    async confirmArticleMove() {\n        if (!this.state.selectedParentArticle){\n            // return if no data selectedParentArticle in the SelectMenu\n            return;\n        }\n        const selectedParentArticle = this.state.selectedParentArticle.value.parentArticleId;\n        const params = {};\n        if (typeof selectedParentArticle === 'number') {\n            params.parent_id = selectedParentArticle;\n        } else {\n            params.category = selectedParentArticle;\n        }\n        await this.orm.call(\n            'knowledge.article',\n            'move_to',\n            [this.props.knowledgeArticleRecord.resId],\n            params\n        );\n        // Reload the current article to apply changes\n        await this.props.knowledgeArticleRecord.model.load();\n        this.props.close();\n    }\n\n    get loggedUserPicture() {\n        return `/web/image?model=res.users&field=avatar_128&id=${user.userId}`;\n    }\n\n    get moveArticleLabel() {\n        return _t('Move \"%s\" under:', this.props.knowledgeArticleRecord.data.display_name);\n    }\n}\n\nexport default MoveArticleDialog;\n", "/** @odoo-module **/\n\nimport { user } from \"@web/core/user\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { ConfirmationDialog, AlertDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from '@web/core/utils/hooks';\nimport { Component, onWillStart, useEffect, useState} from \"@odoo/owl\";\n\nconst permissionLevel = {'none': 0, 'read': 1, 'write': 2}\nconst restrictMessage = _t(\"Are you sure you want to restrict access to this article? \"\n+ \"This means it will no longer inherit access rights from its parents.\");\nconst loseWriteMessage = _t('Are you sure you want to remove your own \"Write\" access?');\n\nexport class PermissionPanel extends Component {\n    static template = \"knowledge.PermissionPanel\";\n    static props = {\n        record: Object,\n    };\n\n    /**\n     * @override\n     */\n    setup () {\n        this.actionService = useService('action');\n        this.dialog = useService('dialog');\n        this.orm = useService('orm');\n        /** @type {import(\"models\").Store} */\n        this.mailStore = useService(\"mail.store\");\n\n        this.state = useState({\n            loading: true,\n            partner_id: user.partnerId\n        });\n        onWillStart(async () => {\n            this.isInternalUser = await user.hasGroup('base.group_user');\n        });\n        useEffect(() => {\n            this.loadPanel();\n        }, () => [this.props.record.resId]);\n    }\n\n    async loadPanel () {\n        Object.assign(this.state, {\n            ...await this.loadData(),\n            loading: false\n        });\n    }\n\n    /**\n     * @returns {Object}\n     */\n    loadData () {\n        return rpc(\"/knowledge/get_article_permission_panel_data\",\n            {\n                article_id: this.props.record.resId\n            }\n        );\n    }\n\n    /**\n     * @returns {Array[Array]}\n     */\n    getInternalPermissionOptions () {\n        return this.state.internal_permission_options;\n    }\n\n    /**\n     * @param {Proxy} member\n     * @returns {Boolean}\n     */\n    isLoggedUser (member) {\n        return member.partner_id === user.partnerId;\n    }\n\n    _onInviteMembersClick () {\n        this.env._saveIfDirty();\n        this.actionService.doAction('knowledge.knowledge_invite_action_from_article', {\n            additionalContext: {active_id: this.props.record.resId},\n            onClose: async () => {\n                // Update panel content\n                await this.loadPanel();\n                // Reload record\n                this.env.model.root.load();\n\n            }\n        });\n    }\n\n    /**\n     * Callback function called when the internal permission of the article changes.\n     * @param {Event} event\n     */\n    _onChangeInternalPermission (event) {\n        const select = event.target;\n        const index = this.state.members.findIndex(current => {\n            return current.partner_id === user.partnerId;\n        });\n        const newPermission = select.value;\n        const oldPermission = this.state.internal_permission;\n        const willRestrict = this.state.based_on && permissionLevel[newPermission] < permissionLevel[oldPermission]\n                                && permissionLevel[newPermission] < permissionLevel[this.state.parent_permission];\n        const willLoseAccess = select.value === 'none' && (index >= 0 && this.state.members[index].permission === 'none');\n        const confirm = async () => {\n            const res = await rpc('/knowledge/article/set_internal_permission',\n                {\n                    article_id: this.props.record.resId,\n                    permission: newPermission,\n                }\n            );\n            if (this._onChangedPermission(res, willLoseAccess)) {\n                this.loadPanel();\n            }\n        };\n\n        if (!willLoseAccess && !willRestrict) {\n            confirm();\n            return;\n        }\n\n        const discard = () => {\n            select.value = oldPermission;\n            this.loadPanel();\n        };\n        const loseAccessMessage = _t('Are you sure you want to set the internal permission to \"none\"? If you do, you will no longer have access to the article.');\n        const confirmLabel = willLoseAccess ? _t('Lose Access') : _t('Restrict Access');\n        const confirmTitle = willLoseAccess ? false : _t('Restrict Access');\n        this._showConfirmDialog(willLoseAccess ? loseAccessMessage : restrictMessage, confirmTitle, { confirmLabel, confirm, cancel: discard });\n    }\n\n    /**\n     * Callback function called when the permission of a user changes.\n     * @param {Event} event\n     * @param {Proxy} member\n     */\n    async _onChangeMemberPermission (event, member) {\n        const index = this.state.members.indexOf(member);\n        if (index < 0) {\n            return;\n        }\n        const select = event.target;\n        const newPermission = select.value;\n        const oldPermission = member.permission;\n        const willLoseAccess = this.isLoggedUser(member) && newPermission === 'none';\n        const willRestrict = this.state.based_on && permissionLevel[newPermission] < permissionLevel[oldPermission];\n        const willLoseWrite = this.isLoggedUser(member) && newPermission !== 'write' && oldPermission === 'write';\n        const willGainWrite = this.isLoggedUser(member) && newPermission === 'write' && oldPermission !== 'write';\n        const confirm = async () => {\n            const res = await rpc('/knowledge/article/set_member_permission',\n                {\n                    article_id: this.props.record.resId,\n                    permission: newPermission,\n                    member_id: member.based_on ? false : member.id,\n                    inherited_member_id: member.based_on ? member.id: false,\n                }\n            );\n            const reloadArticleId = willLoseWrite && !willLoseAccess ? this.props.record.resId : false;\n            if (this._onChangedPermission(res, willLoseAccess || willLoseWrite, reloadArticleId)) {\n                this.loadPanel();\n            }\n        };\n\n        if (!willLoseAccess && !willRestrict && !willLoseWrite) {\n            await confirm();\n            if (willGainWrite) {\n                // Reload article when admin gives himself write access\n                this.env.model.root.load();\n            }\n            return;\n        }\n\n        const discard = () => {\n            select.value = this.state.members[index].permission;\n            this.loadPanel();\n        };\n        const loseAccessMessage = _t('Are you sure you want to set your permission to \"none\"? If you do, you will no longer have access to the article.');\n        const message = willLoseAccess ? loseAccessMessage : willLoseWrite ? loseWriteMessage : restrictMessage ;\n        const title = willLoseAccess ? _t('Leave Article') : _t('Change Permission');\n        const confirmLabel = willLoseAccess ? _t('Lose Access') : this.isLoggedUser(member) ? _t('Restrict own access') : _t('Restrict Access');\n        this._showConfirmDialog(message, title, { confirmLabel, confirm, cancel: discard } );\n    }\n\n    /**\n     * Callback function called when a member is removed.\n     * @param {Event} event\n     * @param {Proxy} member\n     */\n    _onRemoveMember (event, member) {\n        if (!this.state.members.includes(member)) {\n            return;\n        }\n\n        const willRestrict = member.based_on ? true : false;\n        const willLoseAccess = this.isLoggedUser(member) && member.permission !== \"none\";\n        const confirm = async () => {\n            const res = await rpc('/knowledge/article/remove_member',\n                {\n                    article_id: this.props.record.resId,\n                    member_id: member.based_on ? false : member.id,\n                    inherited_member_id: member.based_on ? member.id: false,\n                }\n            );\n            if (this._onChangedPermission(res, willLoseAccess)) {\n                this.loadPanel();\n            }\n        };\n\n        if (!willLoseAccess && !willRestrict) {\n            confirm();\n            return;\n        }\n\n        const discard = () => {\n            this.loadPanel();\n        };\n\n        let message = restrictMessage;\n        let title = _t('Restrict Access');\n        let confirmLabel = title;\n        if (this.isLoggedUser(member) && this.state.category === 'private') {\n            message = _t('Are you sure you want to leave your private Article? As you are its last member, it will be moved to the Trash.');\n            title = _t('Leave Private Article');\n            confirmLabel = _t('Move to Trash');\n        } else if (willLoseAccess) {\n            message = _t('Are you sure you want to remove your member? By leaving an article, you may lose access to it.');\n            title = _t('Leave Article');\n            confirmLabel = _t('Leave');\n        }\n\n        this._showConfirmDialog(message, title, { confirmLabel, confirm, cancel: discard });\n    }\n\n    /**\n     * Callback function called when user clicks on 'Restore' button.\n     * @param {Event} event\n     */\n    _onRestore (event) {\n        const articleId = this.props.record.resId;\n        const confirm = async () => {\n            const res = await this.orm.call(\n                'knowledge.article',\n                'restore_article_access',\n                [[articleId]],\n            );\n            if (res) {\n                if (this._onChangedPermission({success: res})) {\n                    this.loadPanel();\n                }\n            }\n        };\n\n        const message = _t('Are you sure you want to restore access? This means this article will now inherit any access set on its parent articles.');\n        const title = _t('Restore Access');\n        const confirmLabel = _t('Restore Access');\n        this._showConfirmDialog(message, title, { confirmLabel, confirm });\n    }\n\n    /**\n     * @param {Event} event\n     * @param {Proxy} member\n     */\n    async _onMemberAvatarClick (event, member) {\n        if (!member.partner_share) {\n            const partnerRead = await this.orm.read(\n                'res.partner',\n                [member.partner_id],\n                ['user_ids'],\n            );\n            const userIds = partnerRead && partnerRead.length === 1 ? partnerRead[0]['user_ids'] : false;\n            const userId = userIds && userIds.length === 1 ? userIds[0] : false;\n\n            if (userId) {\n                this.mailStore.openChat({ userId });\n            }\n        }\n    }\n\n  /**\n    * This method is called before each permission change rpc when the user needs to confirm the change as them\n    * would lose them access to the article if them do confirm.\n    * @param {str} message\n    * @param {function} confirm\n    * @param {function} discard\n    * @param {str} confirmLabel\n    */\n    _showConfirmDialog (message, title, options) {\n        options = options || {};\n        if (!options.cancel) {\n            options.cancel = this.loadPanel.bind(this);\n        }\n        this.dialog.add(ConfirmationDialog, {\n            title: title || _t(\"Confirmation\"),\n            body: message,\n            ...options\n        });\n    }\n\n  /**\n    * This method is called after each permission change rpc.\n    * It will check if a reloading of the article tree or a complete reload is needed in function\n    * of the new article state (if change of category or if user lost their own access to the current article).\n    * return True if the caller should continue after executing this method, and False, if caller should stop.\n    * @param {Dict} result\n    * @param {Boolean} lostAccess\n    */\n    async _onChangedPermission (result, reloadAll, reloadArticleId) {\n        if (result.error) {\n            this.dialog.add(AlertDialog, {\n                title: _t(\"Error\"),\n                body: result.error,\n            });\n        } else if (reloadAll && reloadArticleId) {  // Lose write access\n            if (await this.props.record.isDirty()) {\n                await this.props.record.save({ reload: false });\n            }\n            await this.env.model.root.load();\n            return false;\n        } else if (reloadAll) {  // Lose access -> Hard Reload\n            window.location.replace('/knowledge/home');\n        } else if (result.new_category) {\n            if (await this.props.record.isDirty()) {\n                await this.props.record.save();\n            }\n            await this.env.model.root.load();\n        }\n        return true;\n    }\n\n    async _onChangeVisibility (event) {\n        const input = event.target;\n        const articleId = this.props.record.resId;\n        await this.orm.call(\n            'knowledge.article',\n            'set_is_article_visible_by_everyone',\n            [articleId, input.value === 'everyone']\n        );\n        if (await this.props.record.isDirty()) {\n            await this.props.record.save();\n        }\n        await this.props.record.load();\n    }\n}\n\nexport default PermissionPanel;\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport {\n    Component,\n    onMounted,\n    useRef } from \"@odoo/owl\";\n\nexport class PromptEmbeddedViewNameDialog extends Component {\n    static template = \"knowledge.PromptEmbeddedViewNameDialog\";\n    static components = { Dialog };\n    static props = {\n        defaultName: { type: String, optional: true },\n        isNew: { type: Boolean, optional: true },\n        viewType: { type: String },\n        save: { type: Function },\n        close: { type: Function, optional: true }\n    };\n\n    /**\n     * @override\n     */\n    setup () {\n        super.setup();\n        this.input = useRef('input');\n        onMounted(() => {\n            window.setTimeout(() => {\n                this.input.el?.focus(); // auto-focus\n            }, 0);\n        });\n    }\n    async save () {\n        await this.props.save(this.input.el.value);\n        this.props.close();\n    }\n    /**\n     * @returns {String}\n     */\n    get placeholder () {\n        if (this.props.viewType === 'kanban') {\n            return _t('e.g. Buildings');\n        }\n        if (this.props.viewType === 'list') {\n            return _t('e.g. Todos');\n        }\n    }\n    /**\n     * @returns {String}\n     */\n    get title () {\n        if (this.props.viewType === 'list') {\n            return _t('Insert a List View');\n        }\n        if (this.props.viewType === 'kanban') {\n            return _t('Insert a Kanban View');\n        }\n        return _t('Embed a View');\n    }\n    /**\n     * @param {Event} event\n     */\n    onInputKeydown (event) {\n        if (event.key === 'Enter') {\n            this.save();\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { registry } from '@web/core/registry';\nimport { standardWidgetProps } from '@web/views/widgets/standard_widget_props';\n\nimport { PropertiesField } from '@web/views/fields/properties/properties_field';\n\nimport { Component, useRef, useState } from '@odoo/owl';\n\nexport class KnowledgeArticleProperties extends Component {\n    static template = 'knowledge.KnowledgeArticleProperties';\n    static props = { ...standardWidgetProps };\n    static components = { PropertiesField };\n\n    setup() {\n        this.state = useState({\n            displayPropertyPanel: false,\n        });\n\n        this.root = useRef('root');\n\n        this.env.bus.addEventListener('KNOWLEDGE:TOGGLE_PROPERTIES', this.toggleProperties.bind(this));\n    }\n\n    get showNoContentHelper() {\n        return this.props.record.data.article_properties.every((prop) => prop.definition_deleted);\n    }\n\n    toggleProperties(event) {\n        this.state.displayPropertyPanel = event.detail.displayPropertyPanel;\n        if (this.state.displayPropertyPanel) {\n            this.root.el?.parentElement?.classList.remove('d-none');\n        } else {\n            this.root.el?.parentElement?.classList.add('d-none');\n        }\n    }\n}\n\n\nexport const knowledgePropertiesPanel = {\n    component: KnowledgeArticleProperties,\n    additionalClasses: [\n        'o_knowledge_properties',\n        'o_field_highlight',\n        'col-12',\n        'col-lg-2',\n        'position-relative',\n        'd-none',\n        'p-0',\n        'border-start'\n    ]\n};\n\nregistry.category('view_widgets').add('knowledge_properties_panel', knowledgePropertiesPanel);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ArticleSelectionDialog } from \"../../components/article_selection_dialog/article_selection_dialog\";\nimport { ArticleTemplatePickerDialog } from \"@knowledge/components/article_template_picker_dialog/article_template_picker_dialog\";\nimport { browser } from '@web/core/browser/browser'\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport {\n    KnowledgeSidebarFavoriteSection,\n    KnowledgeSidebarPrivateSection,\n    KnowledgeSidebarSharedSection,\n    KnowledgeSidebarWorkspaceSection\n} from \"./sidebar_section\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { user } from \"@web/core/user\";\nimport { throttleForAnimation } from \"@web/core/utils/timing\";\nimport { useNestedSortable } from \"@web/core/utils/nested_sortable\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\n\nimport { Component, onWillStart, reactive, useRef, useState, useChildSubEnv } from \"@odoo/owl\";\n\nexport const SORTABLE_TOLERANCE = 10;\n\n/**\n * Main Sidebar component. Its role is mainly to fetch and manage the articles\n * to show and allow to reorder them. It updates the info of the current\n * article each time the props are updated.\n * The articles are stored in the state and have the following shape:\n * - {string} category,\n * - {array} child_ids,\n * - {string} icon,\n * - {boolean} is_article_item,\n * - {boolean} is_locked,\n * - {boolean} is_user_favorite,\n * - {string} name,\n * - {number} parent_id,\n * - {boolean} user_can_write,\n * - {boolean} has_article_children,\n */\nexport class KnowledgeSidebar extends Component {\n    static props = {\n        record: Object,\n    };\n    static template = \"knowledge.Sidebar\";\n    static components = {\n        KnowledgeSidebarFavoriteSection,\n        KnowledgeSidebarPrivateSection,\n        KnowledgeSidebarSharedSection,\n        KnowledgeSidebarWorkspaceSection,\n    };\n    \n    setup() {\n        super.setup();\n\n        this.actionService = useService(\"action\");\n        this.dialog = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n\n        this.favoriteTree = useRef(\"favoriteTree\");\n        this.mainTree = useRef(\"mainTree\");\n\n        this.currentData = {};\n\n        this.storageKeys = {\n            size: \"knowledge.sidebarSize\",\n            unfoldedArticles: \"knowledge.unfolded.ids\",\n            unfoldedFavorites: \"knowledge.unfolded.favorite.ids\",\n        };\n\n        // Get set of unfolded ids and sync it with the local storage (any\n        // change will trigger a write in the local storage)\n        this.unfoldedArticlesIds = reactive(\n            new Set(localStorage.getItem(this.storageKeys.unfoldedArticles)?.split(\";\").map(Number)),\n            () => localStorage.setItem(this.storageKeys.unfoldedArticles, Array.from(this.unfoldedArticlesIds).join(\";\"))\n        );\n        this.unfoldedFavoritesIds = reactive(\n            new Set(localStorage.getItem(this.storageKeys.unfoldedFavorites)?.split(\";\").map(Number)),\n            () => localStorage.setItem(this.storageKeys.unfoldedFavorites, Array.from(this.unfoldedFavoritesIds).join(\";\"))\n        );\n\n        useChildSubEnv({\n            fold: this.fold.bind(this),\n            getArticle: this.getArticle.bind(this),\n            model: this.props.record.model,\n            unfold: this.unfold.bind(this),\n        });\n\n        this.state = useState({\n            dragging: false,\n            sidebarSize: localStorage.getItem(this.storageKeys.size) || 300,\n        });\n\n        this.loadArticles();\n\n        // Resequencing of the favorite articles\n        useNestedSortable({\n            ref: this.favoriteTree,\n            edgeScrolling: {\n                speed: 10,\n                threshold: 15,\n            },\n            preventDrag: (el) => {\n                // Prevent the reordering of child articles that are readonly within the favorite\n                // section.\n                return (\n                    !el.parentElement.classList.contains(\"o_tree\") &&\n                    el.classList.contains(\"readonly\")\n                );\n            },\n            tolerance: SORTABLE_TOLERANCE,\n            onDrop: ({ element, next, parent }) => {\n                if (!parent) {\n                    // Favorite resequence\n                    const articleId = parseInt(element.dataset.articleId);\n                    const beforeId = next ? parseInt(next.dataset.articleId) : false;\n                    this.resequenceFavorites(articleId, beforeId);\n                } else {\n                    // Child of favorite resequence\n                    const article = this.getArticle(parseInt(element.dataset.articleId));\n                    const parentId = parseInt(parent.dataset.articleId);\n                    const currentPosition = {\n                        category: article.category,\n                        parentId: article.parent_id,\n                        beforeArticleId:\n                            parseInt(element.nextElementSibling?.dataset.articleId) || false,\n                    };\n                    const newPosition = {\n                        category: article.category,\n                        parentId: parentId,\n                        beforeArticleId: parseInt(next?.dataset.articleId) || false,\n                    };\n                    this.moveArticle(article, currentPosition, newPosition);\n                }\n            },\n        });\n\n        // Resequencing and rehierarchisation of articles\n        useNestedSortable({\n            ref: this.mainTree,\n            groups: () => this.isInternalUser ? \".o_section\" : \".o_section[data-section='private']\",\n            connectGroups: () => this.isInternalUser,\n            nest: true,\n            preventDrag: (el) => el.classList.contains(\"readonly\"),\n            tolerance: SORTABLE_TOLERANCE,\n            onDragStart: () => this.state.dragging = true,\n            onDragEnd: () => this.state.dragging = false,\n            /**\n             * @param {DOMElement} element - dropped element\n             * @param {DOMElement} next - element before which the element was dropped\n             * @param {DOMElement} group - initial (=current) group of the dropped element\n             * @param {DOMElement} newGroup - group in which the element was dropped\n             * @param {DOMElement} parent - parent element of where the element was dropped\n             * @param {DOMElement} placeholder - hint element showing the current position\n             */\n            onDrop: async ({element, next, group, newGroup, parent, placeholder}) => {\n                const article = this.getArticle(parseInt(element.dataset.articleId));\n                // Dropped on trash, move the article to the trash\n                if (newGroup.classList.contains(\"o_knowledge_sidebar_trash\")) {\n                    this.moveToTrash(article);\n                    return;\n                }\n                const parentId = parent ? parseInt(parent.dataset.articleId) : false;\n                // Dropped on restricted position (child of readonly or shared root)\n                if (placeholder.classList.contains('bg-danger')) {\n                    this.rejectDrop(article, parentId);\n                    return;\n                }\n                const currentPosition = {\n                    category: article.category,\n                    parentId: article.parent_id,\n                    beforeArticleId: parseInt(element.nextElementSibling?.dataset.articleId) || false,\n                };\n                const newPosition = {\n                    category: newGroup.dataset.section,\n                    parentId: parentId,\n                    beforeArticleId: parseInt(next?.dataset.articleId) || false,\n                };\n                this.moveArticle(article, currentPosition, newPosition);\n            },\n            /**\n             * @param {DOMElement} element - moved element\n             * @param {DOMElement} parent - parent element of where the element was moved\n             * @param {DOMElement} group - initial (=current) group of the moved element\n             * @param {DOMElement} newGroup - group in which the element was moved\n             * @param {DOMElement} prevPos.parent - element's parent before the move\n             * @param {DOMElement} placeholder - hint element showing the current position\n             */\n            onMove: ({element, parent, group, newGroup, prevPos, placeholder}) => {\n                if (prevPos.parent) {\n                    const prevParent = this.getArticle(parseInt(prevPos.parent.dataset.articleId));\n                    // Remove caret if article has no child\n                    if (!prevParent.has_article_children ||\n                        prevParent.child_ids.length === 1 &&\n                        prevParent.child_ids[0] === parseInt(element.dataset.articleId)\n                    ) {\n                        prevPos.parent.classList.remove('o_article_has_children');\n                    }\n                }\n                if (parent) {\n                    // Cannot add child to readonly articles, unless it is the\n                    // current parent.\n                    const currentParentId = element.parentElement.parentElement.dataset.articleId;\n                    const targetParentId = parent.dataset.articleId;\n                    if (currentParentId !== targetParentId && parent.classList.contains('readonly')) {\n                        placeholder.classList.add('bg-danger');\n                        return;\n                    }\n                    // Add caret\n                    parent.classList.add('o_article_has_children');\n                } else if (newGroup.dataset.section === \"shared\") {\n                    // Private articles cannot be dropped in the shared section\n                    const article = this.getArticle(parseInt(element.dataset.articleId));\n                    if (article.category === \"private\") {\n                        placeholder.classList.add('bg-danger');\n                        return;\n                    }\n                }\n                placeholder.classList.remove('bg-danger');\n            },\n        });\n\n        onWillStart(async () => {\n            this.isInternalUser = await user.hasGroup('base.group_user');\n        });\n\n        useRecordObserver(async (record) => {\n            const nextDataParentId = record.data.parent_id ? record.data.parent_id[0] : false;\n            // During the first load, `loadArticles` is still pending and the component is in its\n            // loading state. However, because of OWL reactive implementation (uses a Proxy),\n            // record data still has to be read in order to subscribe to later changes, even if\n            // nothing is done with that data on the first call. This subscription is what allows\n            // this callback to be called i.e. when the name of the article is changed, reflecting\n            // the change in the sidebar. See useRecordObserver, effect, reactive implementations\n            // for further details.\n            const article = this.getArticle(record.resId) || {\n                id: record.resId,\n                name: record.data.name,\n                icon: record.data.icon,\n                category: record.data.category,\n                parent_id: nextDataParentId,\n                is_locked: record.data.is_locked,\n                user_can_write: record.data.user_can_write,\n                is_article_item: record.data.is_article_item,\n                is_user_favorite: record.data.is_user_favorite,\n                child_ids: [],\n            };\n            if (this.state.articles[record.resId]) {\n                if (record.data.is_article_item !== article.is_article_item) {\n                    if (record.data.is_article_item) {\n                        // Article became item, remove it from the sidebar\n                        this.removeArticle(article);\n                    } else {\n                        // Item became article, add it in the sidebar\n                        this.insertArticle(article, {\n                            parentId: article.parent_id\n                        });\n                        this.showArticle(article);\n                    }\n                }\n                if (record.data.is_user_favorite !== article.is_user_favorite) {\n                    if (record.data.is_user_favorite) {\n                        // Add the article to the favorites tree\n                        this.state.favoriteIds.push(article.id);\n                    } else {\n                        // Remove the article from the favorites tree\n                        this.state.favoriteIds.splice(this.state.favoriteIds.indexOf(article.id), 1);\n                    }\n                }\n                if ((nextDataParentId !== article.parent_id || record.data.category !== article.category) &&\n                    (record.data.parent_id !== this.currentData.parent_id || record.data.category !== this.currentData.category)) {\n                    // Article changed position (\"Moved to\")\n                    if (!this.getArticle(nextDataParentId)) {\n                        // Parent is not loaded, reload the tree to show moved\n                        // article in the sidebar\n                        await this.loadArticles();\n                        this.showArticle(this.getArticle(record.resId));\n                    } else {\n                        this.repositionArticle(article, {\n                            parentId: nextDataParentId,\n                            category: record.data.category,\n                        });\n                    }\n                }\n                // Update values used to display the current article in the sidebar\n                Object.assign(article, {\n                    name: record.data.name,\n                    icon: record.data.icon,\n                    is_locked: record.data.is_locked,\n                    user_can_write: record.data.user_can_write,\n                    is_article_item: record.data.is_article_item,\n                    is_user_favorite: record.data.is_user_favorite,\n                });\n            } else if (!this.state.loading) {  // New article, add it in the state and sidebar\n                if (record.data.is_user_favorite) {\n                    // Favoriting an article that is not shown in the main\n                    // tree (hidden child, item, or child of restricted)\n                    this.state.favoriteIds.push(record.resId);\n                }\n                this.state.articles[article.id] = article;\n                // Don't add new items in the sidebar\n                if (!record.data.is_article_item) {\n                    await this.insertArticle(article, {\n                        category: article.category,\n                        parentId: article.parent_id,\n                    });\n                    // Make sure the article is visible\n                    this.showArticle(article);\n                    if (nextDataParentId && this.getArticle(nextDataParentId)?.is_user_favorite) {\n                        this.unfold(nextDataParentId, true);\n                    }\n                }\n            }\n\n            this.currentData = {\n                parent_id: record.data.parent_id,\n                category: record.data.category,\n            };\n        });\n        this.env.bus.addEventListener(\"knowledge.sidebar.insertNewArticle\", async ({ detail }) => {\n            if (this.getArticle(detail.articleId)) {\n                // Article already in the sidebar\n                return;\n            }\n            const parent = detail.parentId ? this.getArticle(detail.parentId) : false;\n            const newArticle = {\n                id: detail.articleId,\n                name: detail.name,\n                icon: detail.icon,\n                parent_id: parent ? parent.id : false,\n                category: parent ? parent.category : false,\n                is_locked: false,\n                user_can_write: true,\n                is_article_item: false,\n                is_user_favorite: false,\n                child_ids: [],\n            };\n            this.state.articles[newArticle.id] = newArticle;\n            await this.insertArticle(newArticle, {\n                parentId: newArticle.parent_id,\n                category: parent ? parent.category : false\n            });\n        });\n    }\n\n    /**\n     * Open the templates dialog\n     */\n    browseTemplates() {\n        this.dialog.add(ArticleTemplatePickerDialog, {\n            onLoadTemplate: async articleTemplateId => {\n                await this.actionService.doAction(\"knowledge.ir_actions_server_knowledge_home_page\", {\n                    stackPosition: \"replaceCurrentAction\",\n                    additionalContext: {\n                        res_id: await this.orm.call(\"knowledge.article\", \"create_article_from_template\", [\n                            articleTemplateId\n                        ])\n                    }\n                });\n            }\n        });\n    }\n\n    /**\n     * Change the category of an article, and of all its descendants.\n     * @param {Object} article\n     * @param {String} category\n     */\n    async changeCategory(article, category) {\n        article.category = category;\n        if (article.id === this.props.record.id) {\n            // Reload current record to propagate changes\n            if (await this.props.record.isDirty()) {\n                await this.props.record.save();\n            } else {\n                await this.props.record.model.load();\n            }\n        }\n        for (const childId of article.child_ids) {\n            this.changeCategory(this.getArticle(childId), category);\n        }\n    }\n\n    /**\n     * Create a new article (and open it).\n     * @param {String} - category\n     * @param {integer} - targetParentId\n     */\n    createArticle(category, targetParentId) {\n        try {\n            this.env.createArticle(category, targetParentId);\n        } catch {\n            // Could not create article, reload tree in case some permission changed\n            this.loadArticles();\n        }\n    }\n\n    /**\n     * Fold an article.\n     * @param {integer} articleId: id of article\n     * @param {boolean} isFavorite: whether to fold in favorite tree\n     */\n    fold(articleId, isFavorite) {\n        if (isFavorite) {\n            this.unfoldedFavoritesIds.delete(articleId);\n        } else {\n            this.unfoldedArticlesIds.delete(articleId);\n        }\n    }\n\n    /**\n     * Get the article stored in the state given its id.\n     * @param {integer} articleId - Id of the article \n     * @returns {Object} article\n     */\n    getArticle(articleId) {\n        return this.state.articles[articleId];\n    }\n\n    /**\n     * Get the array of article ids stored in the state from the given category\n     * (eg. this.state.workspaceIds for workspace).\n     * @param {String} category\n     * @returns {Array} array of articles ids\n     */\n    getCategoryIds(category) {\n        return this.state[`${category}Ids`];\n    }\n\n    /**\n     * Insert the given article at the given position in the sidebar.\n     * @param {Object} article - article stored in the state\n     * @param {Object} position\n     * @param {integer} position.beforeArticleId\n     * @param {String} position.category\n     * @param {integer} position.parentId\n     *  \n     */\n    async insertArticle(article, position) {\n        if (position.parentId) {\n            const parent = this.getArticle(position.parentId);\n            if (parent) {\n                // Make sure the existing children are loaded if parent has any\n                if (!this.unfoldedArticlesIds.has(parent.id)) {\n                    await this.unfold(parent.id, false);\n                    if (parent.child_ids.includes(article.id)) {\n                        return;\n                    }\n                }\n                // Position it at the right position w.r. to the other children\n                // if the parent is not yet aware of the child article (eg when\n                // moving, the article is moved in frontend, then if the user\n                // confirms, the article is moved in backend)\n                if (position.beforeArticleId) {\n                    parent.child_ids.splice(parent.child_ids.indexOf(position.beforeArticleId), 0, article.id);\n                } else {\n                    parent.child_ids.push(article.id);\n                }\n                parent.has_article_children = true;\n            }\n        } else {\n            // Add article in the list of articles of the new category, at the right position\n            const categoryIds = this.getCategoryIds(position.category);\n            if (categoryIds) {\n                if (position.beforeArticleId) {\n                    categoryIds.splice(categoryIds.indexOf(position.beforeArticleId), 0, article.id);\n                } else {\n                    categoryIds.push(article.id);\n                }\n            }\n        }\n    }\n\n    /**\n     * Check if the given article is an ancestor of the active one.\n     * @param {integer} articleId\n     * @returns {Boolean}\n     */\n    isAncestor(articleId) {\n        let article = this.getArticle(this.props.record.resId);\n        while (article) {\n            if (article.id === articleId) {\n                return true;\n            }\n            article = this.getArticle(article.parent_id);\n        }\n        return false;\n    }\n\n    /**\n     * Load the articles to show in the sidebar and store them in the state.\n     * One loops through the articles fetched to create a mapping id:article\n     * that allows easy access of the articles, add the articles in their correct categories\n     * and add their children. One uses the parent_id field to fill the \n     * child_ids arrays because a simple read of the child_ids field would\n     * return items (which should not be included in the sidebar), and the\n     * articles would not be sorted correctly.\n     */\n    async loadArticles() {\n        this.state.loading = true;\n        // Remove already loaded articles\n        Object.assign(this.state, {\n            articles: {},\n            favoriteIds: [],\n            workspaceIds: [],\n            sharedIds: [],\n            privateIds: [],\n        });\n        const res = await this.orm.call(\n            this.props.record.resModel,\n            \"get_sidebar_articles\",\n            [this.props.record.resId],\n            { unfolded_ids: [...this.unfoldedArticlesIds, ...this.unfoldedFavoritesIds] }\n        );\n        const children = {};\n        for (const article of res.articles) {\n            this.state.articles[article.id] = {\n                ...article,\n                child_ids: children[article.id] ? children[article.id] : [],\n            };\n            // Items could be shown in the favorite tree as root articles, but\n            // they should not be shown as children of other articles\n            if (!article.is_article_item) {\n                if (article.parent_id) {\n                    const parent = this.getArticle(article.parent_id);\n                    if (parent) {\n                        parent.child_ids.push(article.id);\n                    } else {\n                        // Store children temporarily to add them to the parent\n                        // when the parent will be added to the state in this loop.\n                        if (children[article.parent_id]) {\n                            children[article.parent_id].push(article.id);\n                        } else {\n                            children[article.parent_id] = [article.id];\n                        }\n                    }\n                } else {\n                    this.getCategoryIds(article.category).push(article.id);\n                }\n            }\n        }\n        const ancestorRootArticleId = res.active_article_accessible_root_id;\n        if (ancestorRootArticleId) {\n            this.getCategoryIds(this.getArticle(ancestorRootArticleId).category).push(\n                ancestorRootArticleId\n            );\n        }\n        this.state.favoriteIds = res.favorite_ids;\n        this.showArticle(this.getArticle(this.props.record.resId));\n        this.state.loading = false;\n        this.resetUnfoldedArticles();\n    }\n\n    /**\n     * Load the children of a given article\n     * @param {object} article\n     */\n    async loadChildren(article) {\n        const children = await this.orm.searchRead(\n            this.props.record.resModel,\n            [['parent_id', '=', article.id], ['is_article_item', '=', false]],\n            ['name', 'icon', 'is_locked', 'user_can_write', 'has_article_children'],\n            {\n                'load': 'None',\n                'order': 'sequence, id',\n            }\n        );\n        for (const child of children) {\n            article.child_ids.push(child.id);\n            if (this.getArticle(child.id)) {\n                // Article was already loaded (if it is in the favorites)\n                continue;\n            }\n            this.state.articles[child.id] = {\n                ...child,\n                parent_id: article.id,\n                child_ids: [],\n                category: article.category,\n                is_article_item: false,\n                is_user_favorite: false,\n            };\n        }\n    }\n\n    /**\n     * Try to move the given article to the given position (change its parent/\n     * category/sequence) and update its position in the sidebar.\n     * If the move will change the permissions of the article, show a\n     * confirmation dialog.\n     * @param {Object} article\n     * @param {Object} currentPosition\n     * @param {integer} position.beforeArticleId \n     * @param {String} position.category\n     * @param {integer} position.parentId\n     * @param {Object} newPosition\n     * @param {integer} newPosition.beforeArticleId \n     * @param {String} newPosition.category\n     * @param {integer} newPosition.parentId\n     */\n    async moveArticle(article, currentPosition, newPosition) {\n        const confirmMove = async (article, position) => {\n            if (this.props.record.resId === article.id && await this.props.record.isDirty()) {\n                await this.props.record.save();\n            }\n            try {\n                await this.orm.call(\n                    this.props.record.resModel,\n                    'move_to',\n                    [article.id],\n                    {\n                        category: position.category,\n                        parent_id: position.parentId,\n                        before_article_id: position.beforeArticleId,\n                    }\n                );\n            } catch (error) {\n                // Reload the article tree to show potential modifications\n                // done by another user that could cause the failure.\n                this.loadArticles();\n                throw error;\n            }\n            // Reload the current article as the move will impact its data\n            await this.props.record.model.load();\n        };\n\n        // Move the article in the sidebar\n        this.repositionArticle(article, newPosition);\n        // Permissions won't change, no need to ask for confirmation\n        if (currentPosition.category === newPosition.category) {\n            confirmMove(article, newPosition);\n        } else {\n            // Show confirmation dialog, and move article back to its original\n            // position if the user cancels the move \n            const emoji = article.icon || '';\n            const name = article.name;\n            let message;\n            let confirmLabel;\n            if (newPosition.category === 'workspace') {\n                message = _t(\n                    'Are you sure you want to move \"%(icon)s%(title)s\" to the Workspace? It will be shared with all internal users.',\n                    { icon: emoji, title: name || _t(\"Untitled\") }\n                );\n                confirmLabel = _t(\"Move to Workspace\");\n            } else if (newPosition.category === 'private') {\n                message = _t(\n                    'Are you sure you want to move \"%(icon)s%(title)s\" to private? Only you will be able to access it.',\n                    { icon: emoji, title: name || _t(\"Untitled\") }\n                );\n                confirmLabel = _t(\"Move to Private\");\n            } else if (newPosition.category === 'shared') {\n                if (newPosition.parentId) {\n                    const parent = this.getArticle(newPosition.parentId);\n                    message = _t(\n                        'Are you sure you want to move \"%(icon)s%(title)s\" under \"%(parentIcon)s%(parentTitle)s\"? It will be shared with the same persons.',\n                        {\n                            icon: emoji,\n                            title: name || _t(\"Untitled\"),\n                            parentIcon: parent.icon || \"\",\n                            parentTitle: parent.name || _t(\"Untitled\"),\n                        }\n                    );\n                } else {\n                    message = _t(\n                        'Are you sure you want to move \"%(icon)s%(title)s\" to the Shared section? It will be shared with all listed members.',\n                        { icon: emoji, title: name || _t(\"Untitled\") }\n                    );\n                }\n                confirmLabel = _t('Move to Shared')\n            }\n            this.dialog.add(ConfirmationDialog, {\n                body: message,\n                confirmLabel: confirmLabel,\n                confirm: () => confirmMove(article, newPosition),\n                cancel: () => {\n                    // Move article back to its position\n                    this.repositionArticle(article, currentPosition);\n                },\n            });\n        } \n    }\n\n    /**\n     * Move an article to the trash, and remove it from the sidebar.\n     * @param {Object} article\n     */\n    async moveToTrash(article) {\n        try {\n            await this.orm.call(\n                \"knowledge.article\",\n                \"action_send_to_trash\",\n                [article.id],\n            );\n        } catch {\n            await this.loadArticles();\n            return;\n        }\n        // If the article moved to the trash is an ancestor of the active\n        // article, redirect to first accessible article.\n        if (this.isAncestor(article.id)) {\n            this.actionService.doAction(\n                await this.orm.call('knowledge.article', 'action_home_page', [false]),\n                {stackPosition: 'replaceCurrentAction'}\n            );\n        } else {\n            this.removeArticle(article);\n            this.removeFavorite(article);\n        }\n    }\n\n    /**\n     * Open the command palette if the user is an internal user, and open the\n     * article selection dialog if the user is a portal user\n     */\n    onSearchBarClick() {\n        if (this.isInternalUser) {\n            this.env.services.command.openMainPalette({searchValue: '?'});\n        } else {\n            this.dialog.add(\n                ArticleSelectionDialog,\n                {\n                    title: _t('Search an Article...'),\n                    confirmLabel: _t('Open'),\n                    articleSelected: (article) => this.env.openArticle(article.articleId),\n                }\n            );\n        }\n    }\n\n    /**\n     * Show a dialog explaining why the given article cannot be moved to the\n     * target position.\n     * @param {article}\n     * @param {parentId}\n     */\n    rejectDrop(article, parentId) {\n        let message;\n        if (parentId) {\n            const parent = this.getArticle(parentId);\n            message = _t(\n                'Could not move \"%(icon)s%(title)s\" under \"%(parentIcon)s%(parentTitle)s\", because you do not have write permission on the latter.',\n                {\n                    icon: article.icon || \"\",\n                    title: article.name,\n                    parentIcon: parent.icon || \"\",\n                    parentTitle: parent.name,\n                }\n            );\n        } else {\n            message = _t('You need at least 2 members for the Article to be shared.');\n        }\n        this.dialog.add(ConfirmationDialog, {\n            confirmLabel: _t(\"Close\"),\n            title: _t(\"Move cancelled\"),\n            body: message,\n        });\n    }\n\n    /**\n     * Remove the given article from the sidebar.\n     * @param {Object} article\n     */\n    removeArticle(article) {\n        if (article.parent_id) {\n            // Remove article from array of children of its parent\n            const parent = this.getArticle(article.parent_id);\n            if (parent) {\n                const articleIdx = parent.child_ids.indexOf(article.id);\n                if (articleIdx !== -1) {\n                    parent.child_ids.splice(parent.child_ids.indexOf(article.id), 1);\n                    // Removed last child of the parent article\n                    if (!parent.child_ids.length) {\n                        this.fold(parent.id);\n                        this.fold(parent.id, true);\n                        parent.has_article_children = false;\n                    }\n                }\n            }\n        } else {\n            // Remove article from list of articles category\n            const categoryIds = this.getCategoryIds(article.category);\n            const articleIdx = categoryIds.indexOf(article.id);\n            if (articleIdx !== -1) {\n                categoryIds.splice(articleIdx, 1);\n            }\n        }\n    }\n\n    /**\n     * Remove the given article from the list of favorites.\n     * @param {Object} article\n     */\n    removeFavorite(article) {\n        const favoriteIdx = this.state.favoriteIds.indexOf(article.id);\n        if (favoriteIdx !== -1) {\n            this.state.favoriteIds.splice(favoriteIdx, 1);\n        }\n     }\n\n    /**\n     * Change the position of an article in the sidebar.\n     * @param {Object} article\n     * @param {Object} position\n     * @param {integer} position.beforeArticleId \n     * @param {String} position.category\n     * @param {integer} position.parentId\n     */\n    async repositionArticle(article, position) {\n        this.removeArticle(article);\n        await this.insertArticle(article, position);\n        // Change the parent of the article\n        if (article.parent_id !== position.parentId) {\n            article.parent_id = position.parentId;\n        }\n        // Change category of article and every descendant\n        if (article.category !== position.category) {\n            this.changeCategory(article, position.category);\n        }\n        // Make sure the article is visible\n        this.showArticle(article);\n    }\n\n    /**\n     * Updates the sequence of favorite articles for the current user.\n     * @param {integer} articleId - Id of the moved favorite article\n     * @param {integer} beforeId - Id of the favorite article after\n     *      which the article is moved\n     */\n    resequenceFavorites(articleId, beforeId) {\n        this.state.favoriteIds.splice(this.state.favoriteIds.indexOf(articleId), 1);\n        if (beforeId) {\n            this.state.favoriteIds.splice(this.state.favoriteIds.indexOf(beforeId), 0, articleId);\n        } else {\n            this.state.favoriteIds.push(articleId);\n        }\n        this.orm.call(\"knowledge.article.favorite\", \"resequence_favorites\", [false], {\n            article_ids: this.state.favoriteIds,\n        });\n    }\n\n    /**\n     * User could have unfolded ids in its local storage of articles that are\n     * not shown in its sidebar anymore (trashed, converted to items, hidden,\n     * permission change). This method will reset the list of ids in the local\n     * storage using only the articles that are shown to the user, so that we\n     * do not load the articles using a list containing a lot of useless ids. \n     */\n    resetUnfoldedArticles() {\n        this.unfoldedArticlesIds.forEach(id => {\n            if (!this.getArticle(id)) {\n                this.unfoldedArticlesIds.delete(id);\n            }\n        });\n        this.unfoldedFavoritesIds.forEach(id => {\n            if (!this.getArticle(id)) {\n                this.unfoldedFavoritesIds.delete(id);\n            }\n        });\n    }\n\n    /**\n     * Resize the sidebar horizontally.\n     */\n    resize() {\n        const isRtl = localization.direction === \"rtl\";\n        const onPointerMove = throttleForAnimation(event => {\n            event.preventDefault();\n            this.state.sidebarSize = isRtl ? document.documentElement.clientWidth - event.pageX : event.pageX;\n            browser.dispatchEvent(new Event(\"resize\"));\n        });\n        const onPointerUp = () => {\n            document.removeEventListener('pointermove', onPointerMove);\n            document.body.style.cursor = \"auto\";\n            document.body.style.userSelect = \"auto\";\n            localStorage.setItem(this.storageKeys.size, this.state.sidebarSize);\n            browser.dispatchEvent(new Event(\"resize\"));\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    /**\n     * Make sure the given article is shown in the sidebar by unfolding every\n     * article until a root article is met.\n     * @param {object} article - article to show in the sidebar\n     */\n    showArticle(article) {\n        while (article && article.parent_id && article.parent_id in this.state.articles) {\n            // Unfold in the main tree, without loading the children\n            this.unfold(article.parent_id, false);\n            article = this.getArticle(article.parent_id);\n        }\n    }\n\n    /** Unfold an article.\n     * @param {integer} articleId: id of article\n     * @param {boolean} isFavorite: whether to unfold in favorite tree\n     */        \n    async unfold(articleId, isFavorite) {\n        const article = this.getArticle(articleId);\n        // Load the children of the article if it has not been unfolded yet\n        if (article.has_article_children && !article.child_ids.length) {\n            await this.loadChildren(article);\n        }\n        if (isFavorite) {\n            this.unfoldedFavoritesIds.add(articleId);\n        } else {\n            this.unfoldedArticlesIds.add(articleId);\n        }\n    }\n}\n", "/** @odoo-module */\n\nimport KnowledgeIcon from \"@knowledge/components/knowledge_icon/knowledge_icon\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { Component, onWillUpdateProps, useState } from \"@odoo/owl\";\n\n/**\n * The SidebarRow component is responsible of displaying an article (and its\n * children recursively) in a section of the sidebar, and modifying the record\n * of the article (such as updating the icon).\n */\n\nclass KnowledgeSidebarIcon extends KnowledgeIcon {\n    static props = {\n        article: Object,\n        readonly: Boolean,\n        record: Object,\n        iconClasses: {type: String, optional: true},\n    };\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n    }\n\n    get icon() {\n        return this.props.article.icon || '\ud83d\udcc4';\n    }\n\n    async updateIcon(icon) {\n        if (this.props.record.resId === this.props.article.id) {\n            this.props.record.update({ icon });\n        } else {\n            await this.orm.write(\n                \"knowledge.article\",\n                [this.props.article.id],\n                {icon}\n            );\n            this.props.article.icon = icon;\n        }\n    }\n}\n\nexport class KnowledgeSidebarRow extends Component {\n    static props = {\n        article: Object,\n        unfolded: Boolean,\n        unfoldedIds: Set,\n        record: Object,\n    };\n    static template = \"knowledge.SidebarRow\";\n    static components = {\n        KnowledgeSidebarIcon,\n        KnowledgeSidebarRow\n    };\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n\n        this.state = useState({\n            unfolded: false,\n        });\n\n        onWillUpdateProps(nextProps => {\n            // Remove the loading spinner when the article is rendered as\n            // being unfolded\n            if (this.state.loading && nextProps.unfolded === true) {\n                this.state.loading = false;\n            }\n        });\n    }\n\n    get hasChildren() {\n        return this.props.article.has_article_children;\n    }\n\n    get isActive() {\n        return this.props.record.resId === this.props.article.id;\n    }\n\n    get isLocked() {\n        return this.props.article.is_locked;\n    }\n\n    get isReadonly() {\n        return !this.props.article.user_can_write;\n    }\n\n    /**\n     * Create a new child article for the row's article.\n     */\n    createChild() {\n        this.env.createArticle(this.props.article.category, this.props.article.id);\n    }\n\n    /**\n     * (Un)fold the row\n     */\n    onCaretClick() {\n        if (!this.props.article.has_article_children) {\n            return;\n        }\n        if (this.props.unfolded) {\n            this.env.fold(this.props.article.id);\n        } else if (!this.state.loading) {\n            this.state.loading = true;\n            // If there are a lot of articles, make sure the rendering caused\n            // by the state change and the one cause by the prop update are not\n            // done at once, because otherwise the loader will not be shown.\n            // If there are not too much articles, the renderings can be done\n            // at once so that there is no flickering.\n            if (this.props.article.child_ids.length > 500) {\n                setTimeout(() => this.env.unfold(this.props.article.id), 0);\n            } else {\n                this.env.unfold(this.props.article.id);\n            }\n        }\n    }\n\n    /**\n     * Open the row's article\n     */\n    onNameClick() {\n        this.env.openArticle(this.props.article.id);\n    }\n}\n", "/** @odoo-module */\n\nimport { KnowledgeSidebarRow } from \"./sidebar_row\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { Component, onWillStart, useChildSubEnv } from \"@odoo/owl\";\n\n/**\n * This file defines the different sections used in the sidebar.\n * Each section is responsible of displaying an array of root articles and\n * their children.\n */\n\nexport class KnowledgeSidebarSection extends Component {\n    static template = \"\";\n    static props = {\n        rootIds: Array,\n        unfoldedIds: Set,\n        record: Object,\n    };\n    static components = {\n        KnowledgeSidebarRow,\n    };\n\n    setup() {\n        super.setup();\n        onWillStart(async () => {\n            this.isInternalUser = await user.hasGroup('base.group_user');\n        });\n    }\n}\n\nexport class KnowledgeSidebarFavoriteSection extends KnowledgeSidebarSection {\n    static template = \"knowledge.SidebarFavoriteSection\";\n    \n    setup() {\n        super.setup();\n\n        // (Un)fold in the favorite tree by default.\n        useChildSubEnv({\n            fold: id => this.env.fold(id, true),\n            unfold: id => this.env.unfold(id, true),\n        });\n    }\n}\n\nexport class KnowledgeSidebarWorkspaceSection extends KnowledgeSidebarSection {\n    static template = \"knowledge.SidebarWorkspaceSection\";\n    \n    setup() {\n        super.setup();\n        this.command = useService(\"command\");\n    }\n\n    createRoot() {\n        this.env.createArticle(\"workspace\");\n    }\n\n    searchHiddenArticle() {\n        this.command.openMainPalette({searchValue: \"$\"});\n    }\n}\n\nexport class KnowledgeSidebarSharedSection extends KnowledgeSidebarSection {\n    static template = \"knowledge.SidebarSharedSection\";\n}\n\nexport class KnowledgeSidebarPrivateSection extends KnowledgeSidebarSection {\n    static template = \"knowledge.SidebarPrivateSection\";\n\n    createRoot() {\n        this.env.createArticle(\"private\");\n    }\n}\n", "/** @odoo-module **/\n\nimport { browser } from '@web/core/browser/browser'\nimport { formatDateTime } from '@web/core/l10n/dates';\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from '@web/core/registry';\nimport { standardWidgetProps } from '@web/views/widgets/standard_widget_props';\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useOpenChat } from \"@mail/core/web/open_chat_hook\";\nimport { utils as uiUtils } from \"@web/core/ui/ui_service\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport { Component, onWillStart, useEffect, useRef, useState } from '@odoo/owl';\n\nimport { getRandomIcon } from '@knowledge/js/knowledge_utils';\nimport KnowledgeHierarchy from '@knowledge/components/hierarchy/hierarchy';\nimport MoveArticleDialog from '@knowledge/components/move_article_dialog/move_article_dialog';\nimport PermissionPanel from '@knowledge/components/permission_panel/permission_panel';\nimport { KnowledgeFormStatusIndicator } from \"@knowledge/components/form_status_indicator/form_status_indicator\";\nimport { KNOWLEDGE_READONLY_EMBEDDINGS } from \"@knowledge/editor/embedded_components/embedding_sets\";\nimport { READONLY_MAIN_EMBEDDINGS } from \"@html_editor/others/embedded_components/embedding_sets\";\nimport { HistoryDialog } from \"@html_editor/components/history_dialog/history_dialog\";\n\nclass KnowledgeTopbar extends Component {\n    static template = \"knowledge.KnowledgeTopbar\";\n    static props = {\n        ...standardWidgetProps,\n    };\n    static components = {\n        KnowledgeHierarchy,\n        KnowledgeFormStatusIndicator,\n        PermissionPanel,\n    };\n\n    setup() {\n        super.setup();\n        this.actionService = useService('action');\n        this.dialog = useService('dialog');\n        this.orm = useService('orm');\n        this.uiService = useService('ui');\n\n        this.buttonSharePanel = useRef('sharePanel_button');\n        this.optionsBtn = useRef('optionsBtn');\n\n        this.formatDateTime = formatDateTime;\n\n        this.state = useState({\n            displayChatter: false,\n            displayHistory: false,\n            displayPropertyPanel: !this.articlePropertiesIsEmpty,\n            addingProperty: false,\n            displaySharePanel: false,\n        });\n        this.commentsService = useService(\"knowledge.comments\");\n        this.commentsState = useState(this.commentsService.getCommentsState());\n\n        this.openChat = useOpenChat('res.users');\n\n        onWillStart(async () => {\n            this.isInternalUser = await user.hasGroup('base.group_user');\n        });\n\n        useEffect((optionsBtn) => {\n            // Refresh \"last edited\" and \"create date\" when opening the options\n            // panel\n            if (optionsBtn) {\n                optionsBtn.addEventListener(\n                    'shown.bs.dropdown',\n                    () => this._setDates()\n                );\n            }\n        }, () => [this.optionsBtn.el]);\n\n\n        useEffect(() => {\n            // When opening an article via the sidebar (or when moving one),\n            // display the properties panel if the article has properties and we are not on mobile.\n            if (!uiUtils.isSmall() && !this.articlePropertiesIsEmpty) {\n                this.addProperties();\n            } else if (this.articlePropertiesIsEmpty && this.state.displayPropertyPanel) {\n                // We close the panel if the opened article has no properties and the panel was open.\n                this.toggleProperties();\n            }\n            this.state.addingProperty = false;\n        }, () => [this.props.record.resId, this.articlePropertiesIsEmpty]);\n\n        useEffect((shareBtn) => {\n            if (shareBtn) {\n                shareBtn.addEventListener(\n                    // Prevent hiding the dropdown when the invite modal is shown\n                    'hide.bs.dropdown', (ev) => {\n                        if (this.uiService.activeElement !== document) {\n                            ev.preventDefault();\n                        }\n                });\n                shareBtn.addEventListener(\n                    'shown.bs.dropdown',\n                    () => this.state.displaySharePanel = true\n                );\n                shareBtn.addEventListener(\n                    'hidden.bs.dropdown',\n                    () => this.state.displaySharePanel = false\n                );\n            }\n        }, () => [this.buttonSharePanel.el]);\n    }\n\n    get addFavoriteLabel(){\n        return _t(\"Add to favorites\");\n    }\n\n    get removeFavoriteLabel(){\n        return _t(\"Remove from favorites\");\n    }\n\n    get displayCommentsPanelButton() {\n        return (\n            this.commentsState.displayMode === \"panel\" ||\n            Object.keys(this.commentsState.threadRecords).length\n        );\n    }\n\n    /**\n     * Adds a random cover using unsplash. If unsplash throws an error (service\n     * down/keys unset), opens the cover selector instead.\n     * @param {Event} event\n     */\n    async addCover(event) {\n        // Disable button to prevent multiple calls\n        event.target.classList.add('disabled');\n        this.env.ensureArticleName();\n        let res = {};\n        try {\n            res = await rpc(`/knowledge/article/${this.props.record.resId}/add_random_cover`, {\n                query: this.props.record.data.name,\n                orientation: 'landscape',\n            });\n        } catch (e) {\n            console.error(e);\n        }\n        if (res.cover_id) {\n            await this.props.record.update({cover_image_id: [res.cover_id]});\n        } else {\n            // Unsplash keys unset or rate limit exceeded\n            this.env.openCoverSelector();\n        }\n        event.target.classList.remove('disabled');\n    }\n\n    /**\n     * Add a random icon to the article.\n     * @param {Event} event\n     */\n    async addIcon(event) {\n        this.props.record.update({icon: await getRandomIcon()});\n    }\n\n    _setDates() {\n        if (this.props.record.data.create_date && this.props.record.data.last_edition_date) {\n            this.state.createDate = this.props.record.data.create_date.toRelative();\n            this.state.editionDate = this.props.record.data.last_edition_date.toRelative();\n        }\n    }\n\n    get articlePropertiesIsEmpty() {\n        return this.props.record.data.article_properties.filter((prop) => !prop.definition_deleted).length === 0;\n    }\n\n    toggleProperties() {\n        this.state.displayPropertyPanel = !this.state.displayPropertyPanel;\n        this.env.bus.trigger('KNOWLEDGE:TOGGLE_PROPERTIES', {displayPropertyPanel: this.state.displayPropertyPanel});\n        if (this.state.addingProperty) {\n            this.state.addingProperty = false;\n        }\n    }\n\n    addProperties() {\n        this.state.displayPropertyPanel = true;\n        this.state.addingProperty = true;\n        this.env.bus.trigger('KNOWLEDGE:TOGGLE_PROPERTIES', {displayPropertyPanel: true});\n    }\n\n    toggleComments() {\n        if (this.commentsState.displayMode === \"handler\") {\n            this.commentsState.displayMode = \"panel\";\n        } else {\n            this.commentsState.displayMode = \"handler\";\n        }\n    }\n\n    toggleFullWidth() {\n        this.props.record.update({full_width: !this.props.record.data.full_width});\n        browser.dispatchEvent(new Event(\"resize\"));\n    }\n\n    /**\n     * Copy the current article in private section and open it.\n     */\n    async cloneArticle() {\n        await this.env._saveIfDirty();\n        const articleId = await this.orm.call(\n            'knowledge.article',\n            'action_clone',\n            [this.props.record.resId]\n        );\n        this.env.openArticle(articleId, true);\n    }\n\n    /**\n     * Use the browser print as wkhtmltopdf sadly does not handle emojis / embed views / ...\n     * (Investigation shows that it would be complicated to add that support).\n     *\n     * We load the printing assets of knowledge before asking the window to \"print\".\n     * These assets are loaded dynamically and not included in the base backend assets because they\n     * alter some generic parts of the layout, which other apps may not want.\n     *\n     * (Note that those assets are never \"unloaded\", meaning it requires a reload of the webclient\n     * to remove them, which is considered acceptable as very niche).\n     *\n     */\n    async exportToPdf() {\n        window.print();\n    }\n\n    async setLockStatus(newLockStatus) {\n        await this.props.record.model.root.isDirty();\n        await this.env._saveIfDirty();\n        await this.orm.call(\n            'knowledge.article',\n            `action_set_${newLockStatus ? 'lock' : 'unlock'}`,\n            [this.props.record.resId],\n        );\n        await this.props.record.load();\n    }\n\n    /**\n     * Show the Dialog allowing to move the current article.\n     */\n    async onMoveArticleClick() {\n        await this.env._saveIfDirty();\n        this.dialog.add(MoveArticleDialog, {knowledgeArticleRecord: this.props.record});\n    }\n\n    /**\n     * Show/hide the chatter. When showing it, it fetches data required for\n     * new messages, activities, ...\n     */\n    toggleChatter() {\n        if (this.props.record.resId) {\n            this.state.displayChatter = !this.state.displayChatter;\n            this.env.bus.trigger('KNOWLEDGE:TOGGLE_CHATTER', {displayChatter: this.state.displayChatter});\n        }\n    }\n    /**\n     * Open the history dialog.\n     */\n    async openHistory() {\n        if (this.props.record.resId) {\n            this.state.displayHistory = true;\n            await this.env.model.root.save();\n\n            const versionedFieldName = 'body';\n            const historyMetadata = this.props.record.data[\"html_field_history_metadata\"]?.[versionedFieldName];\n            if (historyMetadata) {\n                this.dialog.add(\n                    HistoryDialog,\n                    {\n                        recordId: this.props.record.resId,\n                        recordModel: this.props.record.resModel,\n                        versionedFieldName: versionedFieldName,\n                        historyMetadata: historyMetadata,\n                        restoreRequested: (html, close) => {\n                            const restoredData = {};\n                            restoredData[versionedFieldName] = html;\n                            this.props.record.update(restoredData);\n                            close();\n                        },\n                        embeddedComponents: [\n                            ...READONLY_MAIN_EMBEDDINGS,\n                            ...KNOWLEDGE_READONLY_EMBEDDINGS,\n                        ],\n                    },\n                    {\n                        onClose: () => this.state.displayHistory = false\n                    }\n                );\n            }\n        }\n    }\n\n    async unarchiveArticle() {\n        await this.orm.call('knowledge.article', 'action_unarchive', [this.props.record.resId]);\n        await this.props.record.load();\n    }\n\n    /**\n     * Set the value of is_article_item for the current record.\n     * @param {boolean} newArticleItemStatus: new is_article_item value\n     */\n    async setIsArticleItem(newArticleItemStatus) {\n        await this.props.record.update({is_article_item: newArticleItemStatus});\n    }\n\n    async sendToTrash() {\n        await this.orm.call('knowledge.article', 'action_send_to_trash', [this.props.record.resId]);\n        await this.actionService.doAction(\n            await this.orm.call('knowledge.article', 'action_redirect_to_parent', [this.props.record.resId]),\n            {stackPosition: 'replaceCurrentAction'}\n        );\n    }\n\n    /**\n     * @param {Event} event\n     * @param {Proxy} member\n     */\n    async _onMemberAvatarClick(event, userId) {\n        event.preventDefault();\n        event.stopPropagation();\n        if (userId) {\n            await this.openChat(userId);\n        }\n    }\n}\n\nexport const knowledgeTopbar = {\n    component: KnowledgeTopbar,\n    fieldDependencies: [\n        { name: \"create_uid\", type: \"many2one\", relation: \"res.users\" },\n        { name: \"html_field_history_metadata\", type: \"jsonb\" },\n        { name: \"last_edition_uid\", type: \"many2one\", relation: \"res.users\" },\n        { name: \"parent_path\", type: \"char\" },\n        { name: \"root_article_id\", type: \"many2one\", relation: \"knowledge.article\" },\n    ],\n};\n\nregistry.category('view_widgets').add('knowledge_topbar', knowledgeTopbar);\n", "import { Component, onMounted, onWillUnmount, useRef, useState } from \"@odoo/owl\";\nimport { setIntersectionObserver } from \"@knowledge/js/knowledge_utils\";\n\nexport class WithLazyLoading extends Component {\n    static template = \"knowledge.WithLazyLoading\";\n    static props = {\n        slots: { type: Object },\n    };\n\n    setup() {\n        this.loader = useRef(\"loader\");\n        this.state = useState({ isLoaded: false });\n\n        onMounted(() => {\n            const { el } = this.loader;\n            this.observer = setIntersectionObserver(el, () => {\n                this.state.isLoaded = true;\n            });\n        });\n\n        onWillUnmount(() => {\n            this.observer.disconnect();\n        });\n    }\n}\n", "import { Component, useSubEnv, xml } from \"@odoo/owl\";\n\n/**\n * Loads the given props to the environment of the nested components.\n *\n * Usage:\n * <WithSubEnv prop1=\"value1\" prop2=\"value2\">\n *    <MyComponent/>\n * </WithSubEnv>\n *\n * MyComponent will then be able to access `prop1` and `prop2` from its environment.\n * (i.e: with `this.env.prop1` and `this.env.prop2`)\n */\nexport class WithSubEnv extends Component {\n    static template = xml`<t t-slot=\"default\"/>`;\n    static props = [\"*\"];\n    setup() {\n        useSubEnv(this.props);\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { ChatGPTPromptDialog } from \"@html_editor/main/chatgpt/chatgpt_prompt_dialog\";\nimport { parseHTML } from \"@html_editor/utils/html\";\nimport { ArticleTemplatePickerDialog } from \"@knowledge/components/article_template_picker_dialog/article_template_picker_dialog\";\nimport { ItemCalendarPropsDialog } from \"@knowledge/components/item_calendar_props_dialog/item_calendar_props_dialog\";\nimport { PromptEmbeddedViewNameDialog } from \"@knowledge/components/prompt_embedded_view_name_dialog/prompt_embedded_view_name_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { renderToFragment } from \"@web/core/utils/render\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class WysiwygArticleHelper extends Component {\n    static template = \"knowledge.WysiwygArticleHelper\";\n    static props = {\n        editor: { type: Object },\n        isVisible: { type: Boolean },\n        record: { type: Object },\n    };\n\n    setup() {\n        this.actionService = useService(\"action\");\n        this.dialogService = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n    }\n\n    onLoadTemplateBtnClick() {\n        this.dialogService.add(ArticleTemplatePickerDialog, {\n            onLoadTemplate: async (articleTemplateId) => {\n                const body = await this.orm.call(\n                    \"knowledge.article\",\n                    \"apply_template\",\n                    [this.props.record.resId],\n                    {\n                        template_id: articleTemplateId,\n                        skip_body_update: true,\n                    }\n                );\n                this.props.editor.editable.replaceChildren(\n                    parseHTML(this.props.editor.document, body)\n                );\n                this.props.editor.shared.selection.setCursorEnd(this.props.editor.editable);\n                this.props.editor.shared.history.addStep();\n                // TODO: apply_template could return all modified values on the current\n                // article and record.update would reload related components\n                await this.actionService.doAction(\n                    \"knowledge.ir_actions_server_knowledge_home_page\",\n                    {\n                        stackPosition: \"replaceCurrentAction\",\n                        additionalContext: {\n                            res_id: this.props.record.resId,\n                        },\n                    }\n                );\n            },\n        });\n    }\n\n    onBuildItemCalendarBtnClick() {\n        this.dialogService.add(ItemCalendarPropsDialog, {\n            isNew: true,\n            knowledgeArticleId: this.props.record.resId,\n            saveItemCalendarProps: async (name, itemCalendarProps) => {\n                const title = name || _t(\"Article Items\");\n                const displayName = name\n                    ? _t(\"Calendar of %s\", name)\n                    : _t(\"Calendar of Article Items\");\n                const embeddedProps = {\n                    viewProps: {\n                        additionalViewProps: { itemCalendarProps },\n                        actionXmlId: \"knowledge.knowledge_article_action_item_calendar\",\n                        displayName: displayName,\n                        viewType: \"calendar\",\n                        context: {\n                            active_id: this.props.record.resId,\n                            default_parent_id: this.props.record.resId,\n                            default_is_article_item: true,\n                        },\n                    },\n                };\n                const fragment = renderToFragment(\"knowledge.ArticleItemTemplate\", {\n                    embeddedProps: JSON.stringify(embeddedProps),\n                    title,\n                });\n                this.props.editor.editable.replaceChildren(...fragment.children);\n                this.props.editor.shared.selection.setCursorEnd(this.props.editor.editable);\n                this.props.editor.shared.history.addStep();\n                this.props.record.update({ name: title });\n            },\n        });\n    }\n\n    onBuildItemKanbanBtnClick() {\n        this.dialogService.add(PromptEmbeddedViewNameDialog, {\n            isNew: true,\n            viewType: \"kanban\",\n            /**\n             * @param {string} name\n             */\n            save: async (name) => {\n                const embeddedProps = {\n                    viewProps: {\n                        actionXmlId: \"knowledge.knowledge_article_item_action_stages\",\n                        displayName: name\n                            ? _t(\"Kanban of %s\", name)\n                            : _t(\"Kanban of Article Items\"),\n                        viewType: \"kanban\",\n                        context: {\n                            active_id: this.props.record.resId,\n                            default_parent_id: this.props.record.resId,\n                            default_is_article_item: true,\n                        },\n                    },\n                };\n                const title = name || _t(\"Article Items\");\n                await this.orm.call(\"knowledge.article\", \"create_default_item_stages\", [\n                    this.props.record.resId,\n                ]);\n                const fragment = renderToFragment(\"knowledge.ArticleItemTemplate\", {\n                    embeddedProps: JSON.stringify(embeddedProps),\n                    title,\n                });\n                this.props.editor.editable.replaceChildren(...fragment.children);\n                this.props.editor.shared.selection.setCursorEnd(this.props.editor.editable);\n                this.props.editor.shared.history.addStep();\n                this.props.record.update({ name: title });\n            },\n        });\n    }\n\n    onBuildItemListBtnClick() {\n        this.dialogService.add(PromptEmbeddedViewNameDialog, {\n            isNew: true,\n            viewType: \"list\",\n            /**\n             * @param {string} name\n             */\n            save: async (name) => {\n                const embeddedProps = {\n                    viewProps: {\n                        actionXmlId: \"knowledge.knowledge_article_item_action\",\n                        displayName: name ? _t(\"List of %s\", name) : _t(\"List of Article Items\"),\n                        viewType: \"list\",\n                        context: {\n                            active_id: this.props.record.resId,\n                            default_parent_id: this.props.record.resId,\n                            default_is_article_item: true,\n                        },\n                    },\n                };\n                const title = name || _t(\"Article Items\");\n\n                const fragment = renderToFragment(\"knowledge.ArticleItemTemplate\", {\n                    embeddedProps: JSON.stringify(embeddedProps),\n                    title,\n                });\n                this.props.editor.editable.replaceChildren(...fragment.children);\n                this.props.editor.shared.selection.setCursorEnd(this.props.editor.editable);\n                this.props.editor.shared.history.addStep();\n                this.props.record.update({ name: title });\n            },\n        });\n    }\n\n    onGenerateArticleClick() {\n        this.dialogService.add(ChatGPTPromptDialog, {\n            initialPrompt: _t(\"Write an article about\"),\n            baseContainer: \"P\",\n            insert: (fragment) => {\n                const generatedContentTitle = fragment.querySelector(\"h1,h2\");\n                const articleTitle = this.props.editor.document.createElement(\"h1\");\n                if (generatedContentTitle && generatedContentTitle.tagName !== \"H1\") {\n                    articleTitle.innerText = generatedContentTitle.innerText;\n                    generatedContentTitle.replaceWith(articleTitle);\n                } else if (!generatedContentTitle) {\n                    const br = this.props.editor.document.createElement(\"BR\");\n                    articleTitle.replaceChildren(br);\n                    fragment.prepend(articleTitle);\n                }\n                this.props.editor.editable.replaceChildren(...fragment.children);\n                this.props.editor.shared.selection.setCursorEnd(this.props.editor.editable);\n                this.props.editor.shared.history.addStep();\n            },\n            sanitize: (fragment) => {\n                return DOMPurify.sanitize(fragment, {\n                    IN_PLACE: true,\n                    ADD_TAGS: [\"#document-fragment\"],\n                    ADD_ATTR: [\"contenteditable\"],\n                });\n            },\n        });\n    }\n}\n", "import { onWillStart, useState } from \"@odoo/owl\";\nimport {\n    getEmbeddedProps,\n    useEmbeddedState,\n    StateChangeManager,\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 { useService } from \"@web/core/utils/hooks\";\nimport { ReadonlyEmbeddedArticleIndexComponent } from \"@knowledge/editor/embedded_components/core/article_index/readonly_article_index\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\n\nexport class EmbeddedArticleIndexComponent extends ReadonlyEmbeddedArticleIndexComponent {\n    static template = \"knowledge.EmbeddedArticleIndex\";\n    static components = {\n        ...ReadonlyEmbeddedArticleIndexComponent.components,\n        EmbeddedComponentToolbar,\n        EmbeddedComponentToolbarButton,\n    };\n    static props = {\n        ...ReadonlyEmbeddedArticleIndexComponent.props,\n        host: { type: Object },\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.embeddedState = useEmbeddedState(this.props.host);\n        this.keepLastFetch = new KeepLast();\n        this.state = useState({\n            loading: false,\n        });\n        onWillStart(async () => {\n            if (this.embeddedState.articles === undefined) {\n                this.loadArticleIndex({ firstLoad: true });\n            }\n        });\n    }\n\n    /**\n     * @param {integer} resId\n     * @param {Boolean} showAllChildren\n     * @returns {Array[Object]}\n     */\n    async fetchAllArticles(resId, showAllChildren) {\n        const domain = [\n            [\"parent_id\", !showAllChildren ? \"=\" : \"child_of\", resId],\n            [\"is_article_item\", \"=\", false],\n        ];\n        const { records } = await this.orm.webSearchRead(\"knowledge.article\", domain, {\n            specification: {\n                display_name: {},\n                parent_id: {},\n            },\n            order: \"sequence\",\n        });\n        return records;\n    }\n\n    async loadArticleIndex({ showAllChildren = undefined, firstLoad = false } = {}) {\n        this.state.loading = true;\n        if (showAllChildren === undefined) {\n            showAllChildren = this.embeddedState.showAllChildren;\n        }\n        const resId = this.env.model.root.resId;\n        const promise = this.fetchAllArticles(resId, showAllChildren);\n        const articles = await this.keepLastFetch.add(promise);\n        if (firstLoad && this.embeddedState.articles !== undefined) {\n            // Articles were provided by a collaborator before\n            // the first load was finished, discard loaded articles.\n            this.state.loading = false;\n            return;\n        }\n        /**\n         * @param {integer} parentId\n         * @returns {Object}\n         */\n        const buildIndex = (parentId) => {\n            return articles\n                .filter((article) => {\n                    return article.parent_id && article.parent_id === parentId;\n                })\n                .map((article) => {\n                    return {\n                        id: article.id,\n                        name: article.display_name,\n                        childIds: buildIndex(article.id),\n                    };\n                });\n        };\n        this.state.loading = false;\n        this.embeddedState.showAllChildren = showAllChildren;\n        this.embeddedState.articles = buildIndex(resId);\n    }\n\n    async onSwitchModeBtnClick() {\n        this.loadArticleIndex({\n            showAllChildren: !this.embeddedState.showAllChildren,\n        });\n    }\n\n    async onRefreshBtnClick() {\n        this.loadArticleIndex();\n    }\n}\n\nexport const articleIndexEmbedding = {\n    name: \"articleIndex\",\n    Component: EmbeddedArticleIndexComponent,\n    getStateChangeManager: (config) => {\n        return new StateChangeManager(config);\n    },\n    getProps: (host) => {\n        return {\n            host,\n            ...getEmbeddedProps(host),\n        };\n    },\n};\n", "import { SIGNATURE_CLASS } from \"@html_editor/main/signature_plugin\";\nimport {\n    getEditableDescendants,\n    getEmbeddedProps,\n} from \"@html_editor/others/embedded_component_utils\";\nimport { isEmptyBlock } from \"@html_editor/utils/dom_info\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { EmbeddedClipboardComponent } from \"@knowledge/editor/embedded_components/core/clipboard/embedded_clipboard\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { SendAsMessageMacro, UseAsDescriptionMacro } from \"@knowledge/macros/clipboard_macros\";\nimport { markup } from \"@odoo/owl\";\n\nexport class MacrosEmbeddedClipboardComponent extends EmbeddedClipboardComponent {\n    static template = \"knowledge.MacrosEmbeddedClipboard\";\n\n    setup() {\n        super.setup();\n        this.actionService = useService(\"action\");\n        this.dialogService = useService(\"dialog\");\n        this.knowledgeCommandsService = useService(\"knowledgeCommandsService\");\n        this.orm = useService(\"orm\");\n        this.uiService = useService(\"ui\");\n        this.macrosServices = {\n            action: this.actionService,\n            dialog: this.dialogService,\n            ui: this.uiService,\n        };\n        this.targetRecordInfo = this.knowledgeCommandsService.getCommandsRecordInfo();\n        this.htmlFieldTargetMessage = _t(\n            \"Use as %s\",\n            this.targetRecordInfo?.fieldInfo?.string || \"Description\"\n        );\n    }\n\n    //--------------------------------------------------------------------------\n    // TECHNICAL\n    //--------------------------------------------------------------------------\n\n    /**\n     * Create a dataTransfer object with the editable content of the template\n     * block, to be used for a paste event in the editor\n     */\n    createHtmlDataTransfer() {\n        const dataTransfer = new DataTransfer();\n        const content = this.descendants.clipboardContent;\n        const value = `<p></p>${content.innerHTML}<p></p>`;\n        dataTransfer.setData(\"application/vnd.odoo.odoo-editor\", value);\n        return dataTransfer;\n    }\n\n    //--------------------------------------------------------------------------\n    // HANDLERS\n    //--------------------------------------------------------------------------\n\n    /**\n     * Callback function called when the user clicks on the \"Send as Message\" button.\n     * The function executes a macro that opens the latest form view, composes a\n     * new message and attaches the associated file to it.\n     * @param {Event} ev\n     */\n    async onClickSendAsMessage(ev) {\n        const rows = await this.orm.read(\"res.users\", [user.userId], [\"signature\"]);\n        const signature = renderToElement(\"html_editor.Signature\", {\n            signature: markup(rows[0]?.signature || \"\"),\n            signatureClass: SIGNATURE_CLASS,\n        });\n\n        const dataTransfer = new DataTransfer();\n        dataTransfer.setData(\"application/vnd.odoo.odoo-editor\",\n            this.descendants.clipboardContent.innerHTML + (\n                isEmptyBlock(signature) ? \"\" : signature.outerHTML));\n\n        const macro = new SendAsMessageMacro({\n            targetXmlDoc: this.targetRecordInfo.xmlDoc,\n            breadcrumbs: this.targetRecordInfo.breadcrumbs,\n            data: {\n                dataTransfer: dataTransfer,\n            },\n            services: this.macrosServices,\n        });\n        macro.start();\n    }\n\n    /**\n     * Callback function called when the user clicks on the \"Use As ...\" button.\n     * The function executes a macro that opens the latest form view containing\n     * a valid target field (see `KNOWLEDGE_RECORDED_FIELD_NAMES`) and copy/past\n     * the content of the template to it.\n     * @param {Event} ev\n     */\n    onClickUseAsDescription(ev) {\n        const dataTransfer = this.createHtmlDataTransfer();\n        const macro = new UseAsDescriptionMacro({\n            targetXmlDoc: this.targetRecordInfo.xmlDoc,\n            breadcrumbs: this.targetRecordInfo.breadcrumbs,\n            data: {\n                fieldName: this.targetRecordInfo.fieldInfo.name,\n                pageName: this.targetRecordInfo.fieldInfo.pageName,\n                dataTransfer: dataTransfer,\n            },\n            services: this.macrosServices,\n        });\n        macro.start();\n    }\n}\n\nexport const macrosClipboardEmbedding = {\n    name: \"clipboard\",\n    Component: MacrosEmbeddedClipboardComponent,\n    getProps: (host) => {\n        return { host, ...getEmbeddedProps(host) };\n    },\n    getEditableDescendants: getEditableDescendants,\n};\n", "import {\n    getEmbeddedProps,\n    useEmbeddedState,\n    StateChangeManager,\n} from \"@html_editor/others/embedded_component_utils\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { EmbeddedViewLinkPopover } from \"@knowledge/editor/embedded_components/backend/embedded_view_link/embedded_view_link_popover\";\nimport { EmbeddedViewLinkEditDialog } from \"@knowledge/editor/embedded_components/backend/embedded_view_link/embedded_view_link_edit_dialog\";\nimport { ReadonlyViewLinkComponent } from \"@knowledge/editor/embedded_components/backend/embedded_view_link/readonly_embedded_view_link\";\n\nexport class EmbeddedViewLinkComponent extends ReadonlyViewLinkComponent {\n    static template = \"knowledge.EmbeddedViewLink\";\n    static props = {\n        ...ReadonlyViewLinkComponent.props,\n        host: { type: Object },\n        copyViewLink: { type: Function },\n        removeViewLink: { type: Function },\n    };\n\n    setup() {\n        this.actionService = useService(\"action\");\n        this.dialogService = useService(\"dialog\");\n        this.notificationService = useService(\"notification\");\n        this.popover = usePopover(EmbeddedViewLinkPopover, {\n            popoverClass: \"o_edit_menu_popover\",\n        });\n        this.embeddedState = useEmbeddedState(this.props.host);\n    }\n\n    set displayName(value) {\n        this.embeddedState.displayName = value;\n    }\n\n    get displayName() {\n        return this.embeddedState.displayName;\n    }\n\n    set linkStyle(value) {\n        this.embeddedState.linkStyle = value;\n    }\n\n    get linkStyle() {\n        return this.embeddedState.linkStyle;\n    }\n\n    onCopyViewLinkClick() {\n        this.props.copyViewLink();\n        this.popover.close();\n    }\n\n    onEditViewLinkClick() {\n        this.dialogService.add(EmbeddedViewLinkEditDialog, {\n            name: this.displayName || \"\",\n            style: this.linkStyle,\n            onSave: (name, style) => {\n                this.displayName = name;\n                this.linkStyle = style;\n            },\n        });\n        this.popover.close();\n    }\n\n    onRemoveViewLinkClick() {\n        this.props.removeViewLink(this.displayName || \"\");\n        this.popover.close();\n    }\n\n    openEmbeddedViewLinkPopover() {\n        this.popover.open(this.props.host, {\n            name: this.displayName,\n            openViewLink: this.openViewLink.bind(this),\n            onCopyViewLinkClick: this.onCopyViewLinkClick.bind(this),\n            onEditViewLinkClick: this.onEditViewLinkClick.bind(this),\n            onRemoveViewLinkClick: this.onRemoveViewLinkClick.bind(this),\n        });\n    }\n}\n\nexport const viewLinkEmbedding = {\n    name: \"viewLink\",\n    Component: EmbeddedViewLinkComponent,\n    getProps: (host) => {\n        return { host, ...getEmbeddedProps(host) };\n    },\n    getStateChangeManager: (config) => {\n        return new StateChangeManager(\n            Object.assign(config, {\n                getEmbeddedState: (host) => {\n                    const props = getEmbeddedProps(host);\n                    return {\n                        displayName: props.viewProps.displayName,\n                        linkStyle: props.linkStyle || \"link\",\n                    };\n                },\n                stateToEmbeddedProps: (host, state) => {\n                    const props = getEmbeddedProps(host);\n                    props.viewProps.displayName = state.displayName;\n                    props.linkStyle = state.linkStyle;\n                    return props;\n                },\n            })\n        );\n    },\n};\n", "import { Component, useState, useRef } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { EMBEDDED_VIEW_LINK_STYLES } from \"@knowledge/editor/embedded_components/core/embedded_view_link/embedded_view_link_style\";\n\nexport class EmbeddedViewLinkEditDialog extends Component {\n    static template = \"knowledge.EmbeddedViewLinkEditDialog\";\n    static components = { Dialog };\n    static props = {\n        style: { type: String },\n        close: { type: Function },\n        name: { type: String },\n        onSave: { type: Function },\n    };\n\n    setup() {\n        this.state = useState({\n            name: this.props.name,\n            style: this.props.style,\n        });\n        this.input = useRef(\"input\");\n    }\n\n    //--------------------------------------------------------------------------\n    // GETTERS/SETTERS\n    //--------------------------------------------------------------------------\n\n    get name() {\n        return this.state.name.trim();\n    }\n\n    get styles() {\n        return EMBEDDED_VIEW_LINK_STYLES;\n    }\n\n    //--------------------------------------------------------------------------\n    // HANDLERS\n    //--------------------------------------------------------------------------\n\n    onConfirm() {\n        if (!this.name) {\n            return this.input.el.focus();\n        }\n        this.props.onSave(this.name, this.state.style);\n        this.props.close();\n    }\n\n    updateStyle(style) {\n        this.state.style = style;\n    }\n}\n", "import { Component } from \"@odoo/owl\";\n\nexport class EmbeddedViewLinkPopover extends Component {\n    static template = \"knowledge.EmbeddedViewLinkPopover\";\n    static props = {\n        close: { type: Function },\n        name: { type: String },\n        onCopyViewLinkClick: { type: Function },\n        onEditViewLinkClick: { type: Function },\n        onRemoveViewLinkClick: { type: Function },\n        openViewLink: { type: Function },\n    };\n}\n", "import { Component } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\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\";\nimport { makeContext } from \"@web/core/context\";\n\nexport class ReadonlyViewLinkComponent extends Component {\n    static template = \"knowledge.ReadonlyEmbeddedViewLink\";\n    static props = {\n        viewProps: { type: Object },\n        linkStyle: { type: String, optional: true },\n    };\n    static defaultProps = {\n        linkStyle: \"link\",\n    };\n\n    get displayName() {\n        return this.props.viewProps.displayName;\n    }\n\n    get linkStyle() {\n        return this.props.linkStyle;\n    }\n\n    setup() {\n        this.actionService = useService(\"action\");\n    }\n\n    getLinkClass() {\n        return EMBEDDED_VIEW_LINK_STYLES[this.linkStyle].class;\n    }\n\n    async openViewLink() {\n        const context = makeContext([this.props.viewProps.context || {}]);\n        const action = await this.actionService.loadAction(\n            this.props.viewProps.actWindow || this.props.viewProps.actionXmlId,\n            context\n        );\n        if (action.type !== \"ir.actions.act_window\") {\n            throw new Error(\n                `Invalid action type \"${action.type}\". Expected \"ir.actions.act_window\"`\n            );\n        }\n        if (this.displayName) {\n            action.name = this.displayName;\n            action.display_name = this.displayName;\n        }\n        action.globalState = {\n            searchModel: this.props.viewProps.context.knowledge_search_model_state,\n        };\n        const props = {};\n        if (action.context.orderBy) {\n            try {\n                props.orderBy = JSON.parse(this.action.context.orderBy);\n            } catch {\n                console.error(\"Parsing orderBy failed\");\n            }\n        }\n        this.actionService.doAction(action, {\n            viewType: this.props.viewProps.viewType,\n            props,\n        });\n    }\n}\n\nexport const readonlyViewLinkEmbedding = {\n    name: \"viewLink\",\n    Component: ReadonlyViewLinkComponent,\n    getProps: (host) => {\n        return { ...getEmbeddedProps(host) };\n    },\n};\n", "import {\n    fileEmbedding,\n    EmbeddedFileComponent,\n} from \"@html_editor/others/embedded_components/backend/file/file\";\nimport { MacrosFileMixin } from \"@knowledge/editor/embedded_components/backend/file/macros_file_mixin\";\n\nexport const MacrosEmbeddedFileComponent = MacrosFileMixin(\n    EmbeddedFileComponent,\n    \"knowledge.EmbeddedFile\"\n);\n\nexport const macrosFileEmbedding = {\n    ...fileEmbedding,\n    Component: MacrosEmbeddedFileComponent,\n};\n", "import { rpc } from \"@web/core/network/rpc\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { getDataURLFromFile } from \"@web/core/utils/urls\";\nimport { AttachToMessageMacro, UseAsAttachmentMacro } from \"@knowledge/macros/file_macros\";\n\nexport const MacrosFileMixin = (T, template) => {\n    return class extends T {\n        static template = template;\n\n        setup() {\n            super.setup();\n            this.actionService = useService(\"action\");\n            this.uiService = useService(\"ui\");\n            this.knowledgeCommandsService = useService(\"knowledgeCommandsService\");\n            this.macrosServices = {\n                action: this.actionService,\n                dialog: this.dialogService,\n                ui: this.uiService,\n            };\n            this.targetRecordInfo = this.knowledgeCommandsService.getCommandsRecordInfo();\n        }\n\n        /**\n         * Callback function called when the user clicks on the \"Send as Message\" button.\n         * The function will execute a macro that will open the last opened form view,\n         * compose a new message and attach the associated file to it.\n         * @param {Event} ev\n         */\n        async onClickAttachToMessage(ev) {\n            const dataTransfer = new DataTransfer();\n            try {\n                const response = await window.fetch(this.fileModel.urlRoute);\n                const blob = await response.blob();\n                const file = new File([blob], this.fileModel.name, {\n                    type: blob.type,\n                });\n                /**\n                 * dataTransfer will be used to mimic a drag and drop of\n                 * the file in the target record chatter.\n                 * @see KnowledgeMacro\n                 */\n                dataTransfer.items.add(file);\n            } catch {\n                return;\n            }\n            const macro = new AttachToMessageMacro({\n                targetXmlDoc: this.targetRecordInfo.xmlDoc,\n                breadcrumbs: this.targetRecordInfo.breadcrumbs,\n                data: {\n                    dataTransfer: dataTransfer,\n                },\n                services: this.macrosServices,\n            });\n            macro.start();\n        }\n\n        /**\n         * Callback function called when the user clicks on the \"Use As Attachment\" button.\n         * The function will execute a macro that will open the last opened form view\n         * and add the associated file to the attachments of the chatter.\n         * @param {Event} ev\n         */\n        async onClickUseAsAttachment(ev) {\n            let attachment;\n            try {\n                const response = await window.fetch(this.fileModel.urlRoute);\n                const blob = await response.blob();\n                const dataURL = await getDataURLFromFile(blob);\n                attachment = await rpc(\"/html_editor/attachment/add_data\", {\n                    name: this.fileModel.name,\n                    data: dataURL.split(\",\")[1],\n                    is_image: false,\n                    res_id: this.targetRecordInfo.resId,\n                    res_model: this.targetRecordInfo.resModel,\n                });\n            } catch {\n                return;\n            }\n            if (!attachment) {\n                return;\n            }\n            const macro = new UseAsAttachmentMacro({\n                targetXmlDoc: this.targetRecordInfo.xmlDoc,\n                breadcrumbs: this.targetRecordInfo.breadcrumbs,\n                data: null,\n                services: this.macrosServices,\n            });\n            macro.start();\n        }\n    };\n};\n", "import {\n    readonlyFileEmbedding,\n    ReadonlyEmbeddedFileComponent,\n} from \"@html_editor/others/embedded_components/core/file/readonly_file\";\nimport { MacrosFileMixin } from \"@knowledge/editor/embedded_components/backend/file/macros_file_mixin\";\n\nexport const ReadonlyMacrosEmbeddedFileComponent = MacrosFileMixin(\n    ReadonlyEmbeddedFileComponent,\n    \"knowledge.ReadonlyEmbeddedFile\"\n);\n\nexport const readonlyMacrosFileEmbedding = {\n    ...readonlyFileEmbedding,\n    Component: ReadonlyMacrosEmbeddedFileComponent,\n};\n", "import {\n    ReadonlyEmbeddedViewComponent,\n    readonlyViewEmbedding,\n} from \"@knowledge/editor/embedded_components/backend/view/readonly_embedded_view\";\nimport {\n    applyObjectPropertyDifference,\n    getEmbeddedProps,\n    StateChangeManager,\n    useEmbeddedState,\n} from \"@html_editor/others/embedded_component_utils\";\nimport { ItemCalendarPropsDialog } from \"@knowledge/components/item_calendar_props_dialog/item_calendar_props_dialog\";\nimport { PromptEmbeddedViewNameDialog } from \"@knowledge/components/prompt_embedded_view_name_dialog/prompt_embedded_view_name_dialog\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { uuid } from \"@web/views/utils\";\nimport { useSubEnv } from \"@odoo/owl\";\n\nexport class EmbeddedViewComponent extends ReadonlyEmbeddedViewComponent {\n    setup() {\n        this.embeddedState = useEmbeddedState(this.props.host);\n        if (!this.id) {\n            this.initNewView();\n        }\n        super.setup();\n        useSubEnv({\n            isEmbeddedReadonly: false,\n        });\n        this.dialogService = useService(\"dialog\");\n        useBus(this.env.bus, `KNOWLEDGE_EMBEDDED_${this.id}:EDIT`, () => {\n            if (this.additionalViewProps && Object.keys(this.additionalViewProps).length) {\n                this.editView();\n            } else {\n                this.renameView();\n            }\n        });\n    }\n\n    set additionalViewProps(value) {\n        this.embeddedState.additionalViewProps = value;\n    }\n\n    get additionalViewProps() {\n        return this.embeddedState.additionalViewProps;\n    }\n\n    set displayName(value) {\n        this.embeddedState.displayName = value;\n    }\n\n    get displayName() {\n        return this.embeddedState.displayName;\n    }\n\n    get favoriteFilters() {\n        return this.embeddedState.favoriteFilters;\n    }\n\n    set id(value) {\n        this.embeddedState.id = value;\n    }\n\n    get id() {\n        return this.embeddedState.id;\n    }\n\n    deleteFavoriteFilter(searchItem) {\n        delete this.favoriteFilters[searchItem.description];\n    }\n\n    editView() {\n        if (this.action.res_model === \"knowledge.article\" && this.action.view_mode === \"calendar\") {\n            this.dialogService.add(ItemCalendarPropsDialog, {\n                isNew: false,\n                name: this.displayName,\n                saveItemCalendarProps: (name, itemCalendarProps) => {\n                    this.displayName = name;\n                    this.additionalViewProps.itemCalendarProps = itemCalendarProps;\n                },\n                knowledgeArticleId: this.env.model.root.resId,\n                ...this.additionalViewProps.itemCalendarProps,\n            });\n        } else {\n            throw new Error(\"Cannot edit the view: the dialog is not implemented\");\n        }\n    }\n\n    initNewView() {\n        if (!this.env.model.root.data.full_width) {\n            this.env.model.root.update({ full_width: true });\n        }\n        this.id = uuid();\n        // duplicate optional fields with new unique key\n        const keyOptionalFields = this.props.viewProps.context.keyOptionalFields;\n        if (keyOptionalFields) {\n            const optionalFields = localStorage.getItem(keyOptionalFields);\n            if (optionalFields) {\n                localStorage.setItem(`${keyOptionalFields},${this.id}`, optionalFields);\n            }\n        }\n    }\n\n    renameView() {\n        this.dialogService.add(PromptEmbeddedViewNameDialog, {\n            isNew: false,\n            defaultName: this.displayName,\n            viewType: this.props.viewProps.viewType,\n            save: (name) => {\n                // TODO ABD: make collaborative setDisplayName reactive. It should be possible since\n                // the embeddedState of collaborators receive the new name.\n                this.displayName = name;\n            },\n        });\n    }\n\n    saveFavoriteFilter(filter) {\n        // favorite filters are saved in an object to allow collaborative writes: multiple users\n        // can create a new filter at the same time and all of them should be kept.\n        this.favoriteFilters[filter.name] = filter;\n    }\n}\n\nexport const viewEmbedding = {\n    ...readonlyViewEmbedding,\n    Component: EmbeddedViewComponent,\n    getStateChangeManager: (config) => {\n        return new StateChangeManager(\n            Object.assign(config, {\n                getEmbeddedState: (host) => {\n                    const props = getEmbeddedProps(host);\n                    return {\n                        additionalViewProps: props.viewProps.additionalViewProps || {},\n                        displayName: props.viewProps.displayName,\n                        favoriteFilters: props.viewProps.favoriteFilters || {},\n                        id: props.viewProps.id,\n                    };\n                },\n                propertyUpdater: {\n                    favoriteFilters: (state, previous, next) => {\n                        applyObjectPropertyDifference(\n                            state,\n                            \"favoriteFilters\",\n                            previous.favoriteFilters,\n                            next.favoriteFilters\n                        );\n                    },\n                },\n                stateToEmbeddedProps: (host, state) => {\n                    const props = getEmbeddedProps(host);\n                    props.viewProps.additionalViewProps = state.additionalViewProps;\n                    props.viewProps.displayName = state.displayName;\n                    props.viewProps.favoriteFilters = state.favoriteFilters;\n                    props.viewProps.id = state.id;\n                    return props;\n                },\n            })\n        );\n    },\n};\n", "import { getEmbeddedProps } from \"@html_editor/others/embedded_component_utils\";\nimport { EmbeddedView } from \"@knowledge/views/embedded_view\";\nimport { WithLazyLoading } from \"@knowledge/components/with_lazy_loading/with_lazy_loading\";\nimport { makeContext } from \"@web/core/context\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useOwnDebugContext } from \"@web/core/debug/debug_context\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { CallbackRecorder } from \"@web/search/action_hook\";\nimport { getDefaultConfig } from \"@web/views/view\";\n\nimport {\n    Component,\n    onError,\n    onWillStart,\n    useExternalListener,\n    useState,\n    useSubEnv,\n} from \"@odoo/owl\";\n\nconst VIEW_RECORDS_LIMITS = {\n    kanban: 20,\n    list: 40,\n};\n\nexport class ReadonlyEmbeddedViewComponent extends Component {\n    static components = {\n        EmbeddedView,\n        WithLazyLoading\n    };\n    static props = {\n        host: { type: Object },\n        viewProps: { type: Object },\n    };\n    static template = \"knowledge.EmbeddedView\";\n\n    setup() {\n        super.setup();\n        this.actionService = useService(\"action\");\n        this.notificationService = useService(\"notification\");\n        this.uiService = useService(\"ui\");\n        this.viewFilters = useService(\"knowledgeEmbedViewsFilters\");\n        useOwnDebugContext(); // define a debug context when the developer mode is enable\n        this.state = useState({\n            error: false,\n            isLoaded: false,\n        });\n        const services = this.env.services;\n        const extendedServices = Object.create(services);\n        extendedServices.action = Object.create(services.action);\n        extendedServices.action.switchView = (viewType, props = {}) => {\n            if (props.resId) {\n                this.action.res_id = props.resId;\n            }\n            this.action.globalState = this.getEmbeddedViewGlobalState();\n            this.actionService.doAction(this.action, { viewType });\n        };\n\n        useSubEnv({\n            config: { ...getDefaultConfig(), disableSearchBarAutofocus: true },\n            isEmbeddedView: true,\n            isEmbeddedReadonly: true,\n            services: extendedServices,\n        });\n\n        useBus(this.env.bus, `KNOWLEDGE_EMBEDDED_${this.id}:OPEN`, () => {\n            this.openView();\n        });\n\n        /**\n         * Normally, the editable is the only element in the editor which has the focus.\n         * In this case, it can be useful to delegate the focus at a lower level\n         * (i.e. to use hotkeys with the uiService).\n         *\n         * `activateElement` is used to set the host as an active element in the ui service, this enables\n         * us to contain the events inside the embedded view when it has the focus.\n         *\n         * `deactivateElement` removes the host as an active element, leaving only the document as active\n         * and we come back to the default behavior of the document handling all the events.\n         *\n         * As a reminder, tabindex=\"-1\" must be set in the template for an element to capture\n         * focusin and focusout events.\n         */\n        useExternalListener(this.props.host, \"focusin\", () => {\n            if (!this.props.host.contains(this.uiService.activeElement)) {\n                this.uiService.activateElement(this.props.host);\n            }\n        });\n        useExternalListener(this.props.host, \"focusout\", (event) => {\n            if (!this.props.host.contains(event.relatedTarget)) {\n                this.uiService.deactivateElement(this.props.host);\n            }\n        });\n\n        onWillStart(async () => {\n            await this.loadEmbeddedView();\n        });\n\n        // TODO ABD: better error handling (discard filters)\n        onError((error) => {\n            console.error(error);\n            this.state.error = true;\n        });\n    }\n\n    get embeddedViewProps() {\n        // forces reactivity for additionalViewProps, displayName and\n        // favoriteFilters\n        return {\n            ...this.staticEmbeddedViewProps,\n            additionalViewProps: { ...this.additionalViewProps },\n            displayName: this.displayName,\n            irFilters: this.irFilters,\n        };\n    }\n\n    get additionalViewProps() {\n        return this.props.viewProps.additionalViewProps;\n    }\n\n    get displayName() {\n        return this.props.viewProps.displayName;\n    }\n\n    get favoriteFilters() {\n        return this.props.viewProps.favoriteFilters;\n    }\n\n    get id() {\n        return this.props.viewProps.id;\n    }\n\n    get irFilters() {\n        // TODO ABD: make collaborative filters update reactive since the embedded state is updated,\n        // it should be possible to update the searchModel too.\n        return (Object.values(this.favoriteFilters || {}) || []).map((filter) => ({\n            ...filter,\n            context: JSON.stringify(filter.context),\n        }));\n    }\n\n    async createRecord() {\n        this.saveFilters();\n        // can we use res_id: false and thus make one method for select and create?\n        this.actionService.doAction({\n            res_model: this.action.res_model,\n            type: 'ir.actions.act_window',\n            views: [this.action.views.find(([_, type]) => type === \"form\") || [false, \"form\"]],\n        });\n    }\n\n    deleteFavoriteFilter(searchItem) {\n        this.notificationService.add(\n            _t(\"You are not allowed to delete a favorite filter in this article.\"),\n            {\n                type: \"danger\",\n            }\n        );\n    }\n\n    /**\n     * Extract the SearchModel state of the embedded view\n     *\n     * @returns {Object} globalState\n     */\n    getEmbeddedViewGlobalState() {\n        const callbacks = this.__getGlobalState__.callbacks;\n        let globalState;\n        if (callbacks.length) {\n            globalState = callbacks.reduce((res, callback) => {\n                return { ...res, ...callback() };\n            }, {});\n        }\n        return { searchModel: globalState && globalState.searchModel };\n    }\n\n    async loadEmbeddedView() {\n        // allow access to the SearchModel exported state which contain facets\n        this.__getGlobalState__ = new CallbackRecorder();\n        const context = this.makeEmbeddedContext();\n        this.action = await this.loadAction(context);\n        this.env.config.views = this.action.views;\n        this.env.config.setDisplayName(this.action.name);\n        this.staticEmbeddedViewProps = this.prepareStaticEmbeddedViewProps(context);\n        this.viewFilters.applyFilter(\n            this.actionService.currentController,\n            this.id,\n            this.staticEmbeddedViewProps\n        );\n        this.state.isLoaded = true;\n    }\n\n    async loadAction(context) {\n        // never use an action help with actWindow (could have been added by a malicious user)\n        if (this.props.actWindow?.help) {\n            delete this.props.actWindow.help;\n        }\n        const action = await this.actionService.loadAction(\n            this.props.viewProps.actWindow || this.props.viewProps.actionXmlId,\n            context\n        );\n        if (action.type !== \"ir.actions.act_window\") {\n            throw new Error(\n                `Invalid action type \"${action.type}\". Expected \"ir.actions.act_window\"`\n            );\n        }\n        if (this.displayName) {\n            action.name = this.displayName;\n            action.display_name = this.displayName;\n        }\n        return action;\n    }\n\n    /**\n     * Create the context used by the embedded view.\n     */\n    makeEmbeddedContext() {\n        const context = makeContext([\n            this.props.viewProps.context || {},\n            {\n                knowledgeEmbeddedViewId: this.id,\n            },\n        ]);\n        return context;\n    }\n\n    /**\n     * Open the view in \"full screen\"\n     */\n    openView() {\n        this.saveFilters();\n        const props = this.additionalViewProps || {};\n        if (this.action.context.orderBy) {\n            try {\n                props.orderBy = JSON.parse(this.action.context.orderBy);\n            } catch {\n                console.error(\"Parsing orderBy failed\");\n            }\n        }\n        // make sure name is up to date (could have been updated by collaborative)\n        if (this.displayName !== this.action.display_name) {\n            this.action.display_name = this.displayName;\n            this.action.name = this.displayName;\n        }\n        this.action.globalState = this.getEmbeddedViewGlobalState();\n        this.actionService.doAction(this.action, {\n            viewType: this.props.viewProps.viewType,\n            props,\n            additionalContext: {\n                knowledgeEmbeddedViewId: this.id,\n                isOpenedEmbeddedView: true,\n            },\n        });\n    }\n\n    prepareStaticEmbeddedViewProps(context) {\n        return {\n            context,\n            createRecord: this.createRecord.bind(this),\n            deleteEmbeddedViewFavoriteFilter: this.deleteFavoriteFilter.bind(this),\n            domain: this.action.domain || [],\n            __getGlobalState__: this.__getGlobalState__,\n            globalState: { searchModel: context.knowledge_search_model_state },\n            irFilters: this.irFilters,\n            limit: VIEW_RECORDS_LIMITS[this.props.viewProps.type],\n            loadActionMenus: true,\n            loadIrFilters: true,\n            noContentHelp: this.action.help,\n            resModel: this.action.res_model,\n            saveEmbeddedViewFavoriteFilter: this.saveFavoriteFilter.bind(this),\n            searchViewId: this.action.searchViewId?.[0],\n            selectRecord: this.selectRecord.bind(this),\n            type: this.props.viewProps.viewType,\n        };\n    }\n\n    saveFilters() {\n        this.viewFilters.saveFilters(\n            this.actionService.currentController,\n            this.id,\n            this.getEmbeddedViewGlobalState().searchModel\n        );\n    }\n\n    saveFavoriteFilter(filter) {\n        this.notificationService.add(\n            _t(\"You are not allowed to save a favorite filter in this article.\"),\n            {\n                type: \"danger\",\n            }\n        );\n    }\n\n    selectRecord(resId) {\n        this.saveFilters();\n        if (this.action.res_model === \"knowledge.article\") {\n            this.env.openArticle(resId);\n        } else {\n            this.actionService.doAction({\n                res_id: resId,\n                res_model: this.action.res_model,\n                type: \"ir.actions.act_window\",\n                views: [this.action.views.find((_, type) => type === \"form\") || [false, \"form\"]],\n            });\n        }\n    }\n}\n\nexport const readonlyViewEmbedding = {\n    name: \"view\",\n    Component: ReadonlyEmbeddedViewComponent,\n    getProps: (host) => {\n        return { host, ...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 { macrosClipboardEmbedding } from \"@knowledge/editor/embedded_components/backend/clipboard/macros_embedded_clipboard\";\nimport { macrosFileEmbedding } from \"@knowledge/editor/embedded_components/backend/file/macros_file\";\nimport { readonlyMacrosFileEmbedding } from \"@knowledge/editor/embedded_components/backend/file/readonly_macros_file\";\nimport { articleIndexEmbedding } from \"@knowledge/editor/embedded_components/backend/article_index/article_index\";\nimport { readonlyArticleIndexEmbedding } from \"@knowledge/editor/embedded_components/core/article_index/readonly_article_index\";\nimport { readonlyViewEmbedding } from \"@knowledge/editor/embedded_components/backend/view/readonly_embedded_view\";\nimport { viewEmbedding } from \"@knowledge/editor/embedded_components/backend/view/embedded_view\";\nimport { readonlyViewLinkEmbedding } from \"@knowledge/editor/embedded_components/backend/embedded_view_link/readonly_embedded_view_link\";\nimport { viewLinkEmbedding } from \"@knowledge/editor/embedded_components/backend/embedded_view_link/embedded_view_link\";\n\nexport const KNOWLEDGE_EMBEDDINGS = [\n    articleIndexEmbedding,\n    macrosClipboardEmbedding,\n    macrosFileEmbedding,\n    viewEmbedding,\n    viewLinkEmbedding,\n];\n\nexport const KNOWLEDGE_READONLY_EMBEDDINGS = [\n    macrosClipboardEmbedding,\n    readonlyArticleIndexEmbedding,\n    readonlyMacrosFileEmbedding,\n    readonlyViewEmbedding,\n    readonlyViewLinkEmbedding,\n];\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { withSequence } from \"@html_editor/utils/resource\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { renderToElement } from \"@web/core/utils/render\";\n\nexport class ArticleIndexPlugin extends Plugin {\n    static id = \"articleIndex\";\n    static dependencies = [\"history\", \"dom\"];\n     resources = {\n        user_commands: [\n            {\n                id: \"insertArticleIndex\",\n                title: _t(\"Index\"),\n                description: _t(\"Show nested articles\"),\n                icon: \"fa-list\",\n                run: this.insertArticleIndex.bind(this),\n            },\n        ],\n        powerbox_categories: [\n            withSequence(20, {\n                id: \"knowledge\",\n                name: _t(\"Knowledge\"),\n            }),\n        ],\n        powerbox_items: [\n            {\n                categoryId: \"knowledge\",\n                commandId: \"insertArticleIndex\",\n            }\n        ],\n    };\n\n    insertArticleIndex() {\n        const articleIndexBlueprint = renderToElement(\"knowledge.ArticleIndexBlueprint\");\n        this.dependencies.dom.insert(articleIndexBlueprint);\n        this.dependencies.history.addStep();\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { getBaseContainerSelector } from \"@html_editor/utils/base_container\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { renderToElement } from \"@web/core/utils/render\";\n\nexport class EmbeddedClipboardPlugin extends Plugin {\n    static id = \"embeddedClipboard\";\n    static dependencies = [\"baseContainer\", \"history\", \"dom\", \"selection\"];\n    resources = {\n        user_commands: [\n            {\n                id: \"insertClipboard\",\n                title: _t(\"Clipboard\"),\n                description: _t(\"Add a clipboard section\"),\n                icon: \"fa-pencil-square\",\n                run: this.insertClipboard.bind(this),\n            },\n        ],\n        powerbox_items: [\n            {\n                categoryId: \"media\",\n                commandId: \"insertClipboard\",\n                isAvailable: (selection) =>\n                    !closestElement(selection.anchorNode, \"[data-embedded='clipboard']\"),\n            },\n        ],\n        mount_component_handlers: this.setupNewClipboard.bind(this),\n    };\n\n    insertClipboard() {\n        const baseContainer = this.dependencies.baseContainer.createBaseContainer();\n        const baseContainerNodeName = baseContainer.nodeName;\n        const baseContainerClass = baseContainer.className;\n        const baseContainerSelector = getBaseContainerSelector(baseContainerNodeName);\n        const clipboardBlock = renderToElement(\"knowledge.EmbeddedClipboardBlueprint\", {\n            baseContainerNodeName,\n            baseContainerAttributes: {\n                class: baseContainerClass,\n            },\n        });\n        this.dependencies.dom.insert(clipboardBlock);\n        this.dependencies.selection.setCursorStart(\n            clipboardBlock.querySelector(baseContainerSelector)\n        );\n        this.dependencies.history.addStep();\n    }\n\n    setupNewClipboard({ name, env }) {\n        if (name === \"clipboard\") {\n            Object.assign(env, {\n                editorShared: {\n                    preserveSelection: this.dependencies.selection.preserveSelection,\n                },\n            });\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { boundariesOut } from \"@html_editor/utils/position\";\nimport { EMBEDDED_COMPONENT_PLUGINS } from \"@html_editor/plugin_sets\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class EmbeddedViewLinkPlugin extends Plugin {\n    static id = \"embeddedViewLink\";\n    static dependencies = [\"history\", \"dom\", \"selection\"];\n    resources = {\n        mount_component_handlers: this.setupNewComponent.bind(this),\n    };\n\n    setupNewComponent({ name, props }) {\n        if (name === \"viewLink\") {\n            Object.assign(props, {\n                removeViewLink: (text) => {\n                    this.replaceElementWith(props.host, text);\n                    this.dependencies.history.addStep();\n                },\n                copyViewLink: () => {\n                    const cursors = this.dependencies.selection.preserveSelection();\n                    this.copyElementToClipboard(props.host);\n                    cursors?.restore();\n                },\n            });\n        }\n    }\n\n    replaceElementWith(target, element) {\n        const [anchorNode, anchorOffset, focusNode, focusOffset] = boundariesOut(target);\n        this.dependencies.selection.setSelection({\n            anchorNode,\n            anchorOffset,\n            focusNode,\n            focusOffset,\n        });\n        this.dependencies.dom.insert(element);\n    }\n\n    copyElementToClipboard(element) {\n        const range = document.createRange();\n        range.selectNode(element);\n        const selection = window.getSelection();\n        selection.removeAllRanges();\n        selection.addRange(range);\n        const copySucceeded = document.execCommand(\"copy\");\n        selection.removeAllRanges();\n        if (copySucceeded) {\n            this.services.notification.add(_t(\"Link copied to clipboard.\"), {\n                type: \"success\",\n            });\n        }\n    }\n}\n\nEMBEDDED_COMPONENT_PLUGINS.push(EmbeddedViewLinkPlugin);\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { closestElement } from \"@html_editor/utils/dom_traversal\";\nimport { ItemCalendarPropsDialog } from \"@knowledge/components/item_calendar_props_dialog/item_calendar_props_dialog\";\nimport { PromptEmbeddedViewNameDialog } from \"@knowledge/components/prompt_embedded_view_name_dialog/prompt_embedded_view_name_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { renderToElement } from \"@web/core/utils/render\";\n\nfunction isAvailable(selection) {\n    return !closestElement(selection.anchorNode, \".o_editor_banner, table, [data-embedded]\");\n}\n\nexport class EmbeddedViewPlugin extends Plugin {\n    static id = \"embeddedView\";\n    static dependencies = [\"history\", \"dom\", \"selection\"];\n    resources = {\n        user_commands: [\n            {\n                id: \"insertEmbeddedViewKanban\",\n                title: _t(\"Item Kanban\"),\n                description: _t(\"Insert a Kanban view of article items\"),\n                icon: \"fa-th-large\",\n                run: () => {\n                    this.promptInsertEmbeddedView(\"kanban\", true);\n                },\n            },\n            {\n                id: \"insertEmbeddedViewCards\",\n                title: _t(\"Item Cards\"),\n                description: _t(\"Insert a Card view of article items\"),\n                icon: \"fa-address-card\",\n                run: () => {\n                    this.promptInsertEmbeddedView(\"kanban\");\n                },\n            },\n            {\n                id: \"insertEmbeddedViewList\",\n                title: _t(\"Item List\"),\n                description: _t(\"Insert a List view of article items\"),\n                icon: \"fa-th-list\",\n                run: () => {\n                    this.promptInsertEmbeddedView(\"list\");\n                },\n            },\n            {\n                id: \"insertEmbeddedViewCalendar\",\n                title: _t(\"Item Calendar\"),\n                description: _t(\"Insert a Calendar view of article items\"),\n                icon: \"fa-calendar-plus-o\",\n                run: () => {\n                    this.promptInsertEmbeddedCalendarView();\n                },\n            }\n        ],\n        powerbox_items: [\n            {\n                categoryId: \"knowledge\",\n                commandId: \"insertEmbeddedViewKanban\",\n                isAvailable,\n            },\n            {\n                categoryId: \"knowledge\",\n                commandId: \"insertEmbeddedViewCards\",\n                isAvailable,\n            },\n            {\n                categoryId: \"knowledge\",\n                commandId: \"insertEmbeddedViewList\",\n                isAvailable,\n            },\n            {\n                categoryId: \"knowledge\",\n                commandId: \"insertEmbeddedViewCalendar\",\n                isAvailable,\n            },\n        ],\n    };\n\n    insertEmbeddedView(actionXmlId, name, viewType, additionalViewProps = {}) {\n        const resId = this.config.getRecordInfo().resId;\n        const embeddedViewBlueprint = renderToElement(\"knowledge.EmbeddedViewBlueprint\", {\n            embeddedProps: JSON.stringify({\n                viewProps: {\n                    actionXmlId,\n                    additionalViewProps,\n                    context: {\n                        active_id: resId,\n                        default_parent_id: resId,\n                        default_is_article_item: true,\n                    },\n                    displayName: name,\n                    viewType,\n                },\n            }),\n        });\n        this.dependencies.dom.insert(embeddedViewBlueprint);\n        this.dependencies.history.addStep();\n    }\n\n    promptInsertEmbeddedCalendarView() {\n        let cursor = this.dependencies.selection.preserveSelection();\n        const resId = this.config.getRecordInfo().resId;\n        this.services.dialog.add(\n            ItemCalendarPropsDialog,\n            {\n                isNew: true,\n                knowledgeArticleId: resId,\n                saveItemCalendarProps: (name, itemCalendarProps) => {\n                    cursor = null;\n                    this.insertEmbeddedView(\n                        \"knowledge.knowledge_article_action_item_calendar\",\n                        name,\n                        \"calendar\",\n                        { itemCalendarProps }\n                    );\n                },\n            },\n            {\n                onClose: () => {\n                    cursor?.restore();\n                },\n            }\n        );\n    }\n\n    promptInsertEmbeddedView(viewType, withStages) {\n        let cursor = this.dependencies.selection.preserveSelection();\n        const resId = this.config.getRecordInfo().resId;\n        this.services.dialog.add(\n            PromptEmbeddedViewNameDialog,\n            {\n                isNew: true,\n                save: async (name) => {\n                    cursor = null;\n                    if (withStages) {\n                        await this.services.orm.call(\n                            \"knowledge.article\",\n                            \"create_default_item_stages\",\n                            [resId]\n                        );\n                    }\n                    this.insertEmbeddedView(\n                        `knowledge.knowledge_article_item_action${withStages ? \"_stages\" : \"\"}`,\n                        name,\n                        viewType\n                    );\n                },\n                viewType,\n            },\n            {\n                onClose: () => {\n                    cursor?.restore();\n                },\n            }\n        );\n    }\n}\n", "export { HtmlUpgradeManager } from \"@html_editor/html_migrations/html_upgrade_manager\";\n", "import { registry } from \"@web/core/registry\";\n\n// Handle the conversion of `o_knowledge_behavior_anchor` elements to their\n// `data-embedded` counterpart, when loading the value of a html_field.\nregistry\n    .category(\"html_editor_upgrade\")\n    .category(\"1.0\")\n    .add(\"knowledge\", \"@knowledge/editor/html_migrations/migration-1.0\");\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 { MAIN_PLUGINS } from \"@html_editor/plugin_sets\";\nimport { AutofocusPlugin } from \"@knowledge/editor/plugins/autofocus_plugin/autofocus_plugin\";\nimport { KnowledgeArticlePlugin } from \"@knowledge/editor/plugins/article_plugin/article_plugin\";\nimport { KnowledgeCommentsPlugin } from \"@knowledge/editor/plugins/comments_plugin/comments_plugin\";\nimport { ArticleIndexPlugin } from \"@knowledge/editor/embedded_components/plugins/article_index_plugin/article_index_plugin\";\nimport { EmbeddedClipboardPlugin } from \"@knowledge/editor/embedded_components/plugins/embedded_clipboard_plugin/embedded_clipboard_plugin\";\nimport { EmbeddedViewPlugin } from \"@knowledge/editor/embedded_components/plugins/embedded_view_plugin/embedded_view_plugin\";\nimport { InsertPendingElementPlugin } from \"@knowledge/editor/plugins/insert_pending_element_plugin/insert_pending_element_plugin\";\nimport { EmbeddedFilePlugin } from \"@html_editor/others/embedded_components/plugins/embedded_file_plugin/embedded_file_plugin\";\n\nMAIN_PLUGINS.push(KnowledgeArticlePlugin);\n\nexport const KNOWLEDGE_PLUGINS = [\n    AutofocusPlugin,\n    InsertPendingElementPlugin,\n    KnowledgeCommentsPlugin,\n];\n\nexport const KNOWLEDGE_EMBEDDED_COMPONENT_PLUGINS = [\n    ArticleIndexPlugin,\n    EmbeddedClipboardPlugin,\n    EmbeddedViewPlugin,\n    EmbeddedFilePlugin,\n];\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { rightPos } from \"@html_editor/utils/position\";\nimport { ArticleSelectionDialog } from \"@knowledge/components/article_selection_dialog/article_selection_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { renderToElement } from \"@web/core/utils/render\";\n\nconst ARTICLE_LINKS_SELECTOR = \".o_knowledge_article_link\";\nexport class KnowledgeArticlePlugin extends Plugin {\n    static id = \"article\";\n    static dependencies = [\"history\", \"dom\", \"selection\", \"dialog\"];\n    resources = {\n        user_commands: [\n            {\n                id: \"insertArticle\",\n                title: _t(\"Article\"),\n                description: _t(\"Insert an Article shortcut\"),\n                icon: \"fa-newspaper-o\",\n                run: this.addArticle.bind(this),\n            },\n        ],\n        powerbox_items: [\n            {\n                categoryId: \"navigation\",\n                commandId: \"insertArticle\",\n            },\n        ],\n        clean_for_save_handlers: this.cleanForSave.bind(this),\n        normalize_handlers: this.normalize.bind(this),\n    };\n\n    setup() {\n        super.setup();\n        this.boundOpenArticle = this.openArticle.bind(this);\n    }\n\n    addArticle() {\n        const recordInfo = this.config.getRecordInfo();\n        let parentArticleId;\n        if (recordInfo.resModel === \"knowledge.article\" && recordInfo.resId) {\n            parentArticleId = recordInfo.resId;\n        }\n        this.dependencies.dialog.addDialog(ArticleSelectionDialog, {\n            title: _t(\"Link an Article\"),\n            confirmLabel: _t(\"Insert Link\"),\n            articleSelected: (article) => {\n                const articleLinkBlock = renderToElement(\"knowledge.ArticleBlueprint\", {\n                    href: `/knowledge/article/${article.articleId}`,\n                    articleId: article.articleId,\n                    displayName: article.displayName,\n                });\n\n                this.dependencies.dom.insert(articleLinkBlock);\n                this.dependencies.history.addStep();\n                const [anchorNode, anchorOffset] = rightPos(articleLinkBlock);\n                this.dependencies.selection.setSelection({ anchorNode, anchorOffset });\n            },\n            parentArticleId,\n        });\n    }\n\n    scanForArticleLinks(element) {\n        const articleLinks = [...element.querySelectorAll(ARTICLE_LINKS_SELECTOR)];\n        if (element.matches(ARTICLE_LINKS_SELECTOR)) {\n            articleLinks.unshift(element);\n        }\n        return articleLinks;\n    }\n\n    async openArticle(ev) {\n        if (this.config.embeddedComponentInfo?.env?.openArticle) {\n            const articleId = parseInt(ev.target.dataset.res_id);\n            if (articleId) {\n                ev.preventDefault();\n                await this.config.embeddedComponentInfo.env.openArticle(articleId);\n            }\n        }\n    }\n\n    normalize(element) {\n        const articleLinks = this.scanForArticleLinks(element);\n        for (const articleLink of articleLinks) {\n            articleLink.setAttribute(\"target\", \"_blank\");\n            articleLink.setAttribute(\"contenteditable\", \"false\");\n            articleLink.addEventListener(\"click\", this.boundOpenArticle);\n        }\n    }\n\n    cleanForSave({ root }) {\n        const articleLinks = this.scanForArticleLinks(root);\n        for (const articleLink of articleLinks) {\n            articleLink.removeAttribute(\"contenteditable\");\n        }\n    }\n\n    destroy() {\n        super.destroy();\n        const articleLinks = this.scanForArticleLinks(this.editable);\n        for (const articleLink of articleLinks) {\n            articleLink.removeEventListener(\"click\", this.boundOpenArticle);\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { paragraphRelatedElementsSelector } from \"@html_editor/utils/dom_info\";\n\nexport class AutofocusPlugin extends Plugin {\n    static id = \"autofocus\";\n    static dependencies = [\"selection\"];\n    resources = {\n        start_edition_handlers: this.focusFirstElement.bind(this),\n    };\n\n    focusFirstElement() {\n        for (const paragraph of this.editable.querySelectorAll(paragraphRelatedElementsSelector)) {\n            if (paragraph.isContentEditable) {\n                const { anchorNode, anchorOffset, focusNode, focusOffset } =\n                    this.dependencies.selection.setSelection({\n                        anchorNode: paragraph,\n                        anchorOffset: 0,\n                    });\n                const selectionData = this.dependencies.selection.getSelectionData();\n                if (!selectionData.documentSelectionIsInEditable) {\n                    const selection = this.document.getSelection();\n                    selection.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);\n                }\n                break;\n            }\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { leftPos, rightPos, nodeSize } from \"@html_editor/utils/position\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { CommentBeaconManager } from \"@knowledge/comments/comment_beacon_manager\";\nimport { registry } from \"@web/core/registry\";\nimport { KnowledgeCommentsHandler } from \"@knowledge/comments/comments_handler/comments_handler\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { closestElement, childNodes } from \"@html_editor/utils/dom_traversal\";\nimport { callbacksForCursorUpdate } from \"@html_editor/utils/selection\";\nimport { fillEmpty } from \"@html_editor/utils/dom\";\nimport {\n    isParagraphRelatedElement,\n    isPhrasingContent,\n    isContentEditable,\n    isZwnbsp,\n    isListItemElement,\n} from \"@html_editor/utils/dom_info\";\nimport { uniqueId } from \"@web/core/utils/functions\";\nimport { effect } from \"@web/core/utils/reactive\";\nimport { batched } from \"@web/core/utils/timing\";\nimport { withSequence } from \"@html_editor/utils/resource\";\n\nfunction isAllowedBeaconPosition(node) {\n    return isPhrasingContent(node) || isParagraphRelatedElement(node) || isListItemElement(node);\n}\n\nexport class KnowledgeCommentsPlugin extends Plugin {\n    static id = \"knowledgeComments\";\n    static dependencies = [\n        \"history\",\n        \"dom\",\n        \"protectedNode\",\n        \"selection\",\n        \"position\",\n        \"localOverlay\",\n        \"linkSelection\",\n        \"format\",\n        \"delete\",\n    ];\n    resources = {\n        user_commands: [\n            {\n                id: \"addComments\",\n                icon: \"fa-commenting\",\n                run: this.addCommentToSelection.bind(this),\n            },\n        ],\n        toolbar_groups: [\n            withSequence(60, {\n                id: \"knowledge\",\n            }),\n            withSequence(60, {\n                id: \"knowledge_image\",\n                namespace: \"image\",\n            }),\n        ],\n        toolbar_items: [\n            {\n                id: \"comments\",\n                groupId: \"knowledge\",\n                commandId: \"addComments\",\n                title: _t(\"Add a comment to selection\"),\n                text: _t(\"Comment\"),\n            },\n            {\n                id: \"comments_image\",\n                groupId: \"knowledge_image\",\n                commandId: \"addComments\",\n                title: _t(\"Add a comment to an image\"),\n                text: _t(\"Comment\"),\n            },\n        ],\n\n        /** Handlers */\n        layout_geometry_change_handlers: () => {\n            // TODO ABD: why is this called\n            this.commentBeaconManager?.drawThreadOverlays();\n            this.config.onLayoutGeometryChange();\n        },\n        selectionchange_handlers: (selectionData) => {\n            if (\n                !selectionData.documentSelectionIsInEditable ||\n                !selectionData.editableSelection ||\n                selectionData.documentSelectionIsProtected ||\n                selectionData.documentSelectionIsProtecting\n            ) {\n                return;\n            }\n            const editableSelection = selectionData.editableSelection;\n            const target =\n                editableSelection.anchorNode.nodeType === Node.TEXT_NODE\n                    ? editableSelection.anchorNode\n                    : childNodes(editableSelection.anchorNode).at(editableSelection.anchorOffset);\n            if (!target || !target.isConnected) {\n                return;\n            }\n            this.commentBeaconManager.activateRelatedThread(target);\n        },\n        restore_savepoint_handlers: this.updateBeacons.bind(this),\n        history_reset_handlers: () => this.updateBeacons(),\n        history_reset_from_steps_handlers: this.updateBeacons.bind(this),\n        step_added_handlers: () => this.commentBeaconManager.drawThreadOverlays(),\n        external_step_added_handlers: this.updateBeacons.bind(this),\n        clean_for_save_handlers: this.cleanForSave.bind(this),\n        normalize_handlers: this.normalize.bind(this),\n\n        /** Overrides */\n        // TODO ABD: arbitrary sequence, investigate what makes sense\n        delete_forward_overrides: withSequence(1, this.handleDeleteForward.bind(this)),\n        delete_backward_overrides: withSequence(1, this.handleDeleteBackward.bind(this)),\n\n        intangible_char_for_keyboard_navigation_predicates: this.arrowShouldSkip.bind(this),\n    };\n\n    setup() {\n        // ensure that this plugin is not dependent on the collaboration plugin\n        this.peerId = this.config.collaboration?.peerId ?? \"\";\n        this.commentsService = this.services[\"knowledge.comments\"];\n        this.commentsState = this.commentsService.getCommentsState();\n        // reset pending beacons\n        let previousActiveThreadId;\n        this.alive = true;\n        effect(\n            batched((state) => {\n                if (!this.alive) {\n                    return;\n                }\n                if (previousActiveThreadId === state.activeThreadId) {\n                    return;\n                }\n                if (\n                    previousActiveThreadId !== \"undefined\" &&\n                    state.activeThreadId === \"undefined\"\n                ) {\n                    this.commentBeaconManager.pendingBeacons = new Set();\n                } else if (previousActiveThreadId === \"undefined\") {\n                    this.commentBeaconManager.sortThreads();\n                    if (this.commentBeaconManager.bogusBeacons.size) {\n                        this.commentBeaconManager.removeBogusBeacons();\n                        this.dependencies.history.addStep();\n                    }\n                    this.commentBeaconManager.drawThreadOverlays();\n                }\n                previousActiveThreadId = state.activeThreadId;\n            }),\n            [this.commentsState]\n        );\n        this.localOverlay =\n            this.dependencies.localOverlay.makeLocalOverlay(\"KnowledgeThreadBeacons\");\n        this.commentBeaconManager = new CommentBeaconManager({\n            document: this.document,\n            source: this.editable,\n            overlayContainer: this.localOverlay,\n            commentsState: this.commentsState,\n            peerId: this.peerId,\n            removeBeacon: this.removeBeacon.bind(this),\n            setSelection: this.dependencies.selection.setSelection,\n            onStep: () => {\n                this.dependencies.history.addStep();\n            },\n        });\n        this.addDomListener(window, \"click\", this.onWindowClick.bind(this));\n        this.overlayComponentsKey = uniqueId(\"KnowledgeCommentsHandler\");\n        registry.category(this.config.localOverlayContainers.key).add(\n            this.overlayComponentsKey,\n            {\n                Component: KnowledgeCommentsHandler,\n                props: {\n                    commentBeaconManager: this.commentBeaconManager,\n                    contentRef: {\n                        el: this.editable,\n                    },\n                },\n            },\n            { force: true }\n        );\n    }\n\n    arrowShouldSkip(ev, char, lastSkipped) {\n        if (char !== undefined || lastSkipped !== \"\\uFEFF\") {\n            return;\n        }\n        const selection = this.document.getSelection();\n        if (!selection) {\n            return;\n        }\n        const { anchorNode, focusNode } = selection;\n        if (\n            !this.editable.contains(anchorNode) ||\n            (focusNode !== anchorNode && !this.editable.contains(focusNode))\n        ) {\n            return;\n        }\n        const screenDirection = ev.key === \"ArrowLeft\" ? \"left\" : \"right\";\n        const isRtl = closestElement(focusNode, \"[dir]\")?.dir === \"rtl\";\n        const domDirection = (screenDirection === \"left\") ^ isRtl ? \"previous\" : \"next\";\n        let targetNode;\n        let targetOffset;\n        const range = selection.getRangeAt(0);\n        if (ev.shiftKey) {\n            targetNode = selection.focusNode;\n            targetOffset = selection.focusOffset;\n        } else {\n            if (domDirection === \"previous\") {\n                targetNode = range.startContainer;\n                targetOffset = range.startOffset;\n            } else {\n                targetNode = range.endContainer;\n                targetOffset = range.endOffset;\n            }\n        }\n        if (domDirection === \"previous\") {\n            const beacon = this.identifyPreviousBeacon({\n                endContainer: targetNode,\n                endOffset: targetOffset,\n            });\n            return beacon && this.commentBeaconManager.isDisabled(beacon);\n        } else {\n            const beacon = this.identifyNextBeacon({\n                startContainer: targetNode,\n                startOffset: targetOffset,\n            });\n            return beacon && this.commentBeaconManager.isDisabled(beacon);\n        }\n    }\n\n    identifyNextBeacon(range) {\n        const { startContainer, startOffset } = range;\n        if (startContainer.nodeType !== Node.TEXT_NODE) {\n            return;\n        }\n        let container;\n        if (isZwnbsp(startContainer)) {\n            container = startContainer;\n        } else if (\n            startOffset === nodeSize(startContainer) &&\n            startContainer.nextSibling?.nodeType === Node.TEXT_NODE &&\n            isZwnbsp(startContainer.nextSibling)\n        ) {\n            container = startContainer.nextSibling;\n        }\n        if (container) {\n            const [anchorNode, anchorOffset] = rightPos(container);\n            const target = childNodes(anchorNode).at(anchorOffset);\n            if (target?.matches?.(\".oe_thread_beacon\")) {\n                return target;\n            }\n        }\n    }\n\n    identifyPreviousBeacon(range) {\n        const { endContainer, endOffset } = range;\n        if (endContainer.nodeType !== Node.TEXT_NODE) {\n            return;\n        }\n        let container;\n        if (isZwnbsp(endContainer)) {\n            container = endContainer;\n        } else if (\n            endOffset === 0 &&\n            endContainer.previousSibling?.nodeType === Node.TEXT_NODE &&\n            isZwnbsp(endContainer.previousSibling)\n        ) {\n            // This part of the condition may not be necessary since\n            // it seems that endOffset is never set at 0\n            container = endContainer.previousSibling;\n        }\n        if (container) {\n            const [anchorNode, anchorOffset] = leftPos(container);\n            if (anchorOffset === 0) {\n                return;\n            }\n            const target = childNodes(anchorNode).at(anchorOffset - 1);\n            if (target?.matches?.(\".oe_thread_beacon\")) {\n                return target;\n            }\n        }\n    }\n\n    // TODO ABD: -> CTRL + DELETE needs some custo too (currently deletes too much)\n    handleDeleteForward(range) {\n        // allow deleteForward to go past a beacon instead of being blocked.\n        // TODO ABD: add tests for both cases\n        const target = this.identifyNextBeacon(range);\n        if (target) {\n            const [anchorNode, anchorOffset] = rightPos(target);\n            this.dependencies.selection.setSelection({\n                anchorNode,\n                anchorOffset,\n            });\n            this.dependencies.delete.delete(\"forward\", \"character\");\n            return true;\n        }\n    }\n\n    handleDeleteBackward(range) {\n        // allow deleteBackward to go past a beacon instead of being blocked.\n        const target = this.identifyPreviousBeacon(range);\n        if (target) {\n            const [anchorNode, anchorOffset] = leftPos(target);\n            this.dependencies.selection.setSelection({\n                anchorNode,\n                anchorOffset,\n            });\n            this.dependencies.delete.delete(\"backward\", \"character\");\n            return true;\n        }\n    }\n\n    addCommentToSelection() {\n        // TODO ABD: can this method ever be called if either the start or the\n        // end of the selection are in a non-editable positon ?\n        const { startContainer, startOffset, endContainer, endOffset } =\n            this.dependencies.selection.getEditableSelection({ deep: true });\n        const isCollapsed = startContainer === endContainer && startOffset === endOffset;\n        if (\n            isCollapsed ||\n            !isAllowedBeaconPosition(startContainer) ||\n            !isAllowedBeaconPosition(endContainer) ||\n            !isContentEditable(startContainer) ||\n            !isContentEditable(endContainer)\n        ) {\n            return;\n        }\n        const previousUndefinedBeacons = [\n            ...this.editable.querySelectorAll(\".oe_thread_beacon[data-id='undefined']\"),\n        ];\n        this.commentBeaconManager.pendingBeacons = new Set();\n        const endBeacon = renderToElement(\"knowledge.threadBeacon\", {\n            threadId: \"undefined\",\n            type: \"threadBeaconEnd\",\n            recordId: this.commentsState.articleId,\n            recordModel: \"knowledge.article\",\n            peerId: this.peerId,\n        });\n        this.dependencies.selection.setSelection({\n            anchorNode: endContainer,\n            anchorOffset: endOffset,\n        });\n        this.commentBeaconManager.pendingBeacons.add(endBeacon);\n        this.dependencies.dom.insert(endBeacon);\n        const startBeacon = renderToElement(\"knowledge.threadBeacon\", {\n            threadId: \"undefined\",\n            type: \"threadBeaconStart\",\n            recordId: this.commentsState.articleId,\n            recordModel: \"knowledge.article\",\n            peerId: this.peerId,\n        });\n        this.dependencies.selection.setSelection({\n            anchorNode: startContainer,\n            anchorOffset: startOffset,\n        });\n        this.commentBeaconManager.pendingBeacons.add(startBeacon);\n        this.dependencies.dom.insert(startBeacon);\n        this.commentBeaconManager.cleanupThread(\"undefined\");\n        for (const beacon of previousUndefinedBeacons) {\n            this.removeBeacon(beacon);\n        }\n        const [anchorNode, anchorOffset] = rightPos(startBeacon);\n        this.dependencies.selection.setSelection({\n            anchorNode,\n            anchorOffset,\n        });\n        this.commentsState.displayMode = \"handler\";\n        this.commentsService.createVirtualThread();\n        this.commentsState.activeThreadId = \"undefined\";\n        this.dependencies.history.addStep();\n    }\n\n    onWindowClick(ev) {\n        const selector = `.oe-local-overlay, .o_knowledge_comment_box, .o-we-toolbar, .o-overlay-container`;\n        const closestElement = ev.target.closest(selector);\n        if (!closestElement && !this.editable.contains(ev.target)) {\n            this.commentsState.activeThreadId = undefined;\n        }\n    }\n\n    normalize(elem) {\n        this.commentBeaconManager.sortThreads();\n        // TODO ABD: think about the fact that a beacon can be normalized in different steps\n        // for different users, is this an issue ?\n        this.commentBeaconManager.removeBogusBeacons();\n        for (const beacon of elem.querySelectorAll(\".oe_thread_beacon\")) {\n            if (beacon.isConnected && !isAllowedBeaconPosition(beacon.parentElement)) {\n                // TODO ABD: evaluate cleanupThread ?\n                this.commentBeaconManager.cleanupBeaconPair(beacon.dataset.id);\n                this.removeBeacon(beacon);\n                continue;\n            }\n            this.dependencies.linkSelection.padLinkWithZwnbsp(beacon);\n            this.dependencies.protectedNode.setProtectingNode(beacon, true);\n        }\n    }\n\n    cleanZwnbsp(beacon) {\n        if (!beacon.isConnected) {\n            return;\n        }\n        const cursors = this.dependencies.selection.preserveSelection();\n        if (\n            isZwnbsp(beacon.previousSibling) &&\n            beacon.previousSibling.previousSibling?.nodeName !== \"A\"\n        ) {\n            cursors.update(callbacksForCursorUpdate.remove(beacon.previousSibling));\n            beacon.previousSibling.remove();\n        }\n        if (isZwnbsp(beacon.nextSibling) && beacon.nextSibling.nextSibling?.nodeName !== \"A\") {\n            cursors.update(callbacksForCursorUpdate.remove(beacon.nextSibling));\n            beacon.nextSibling.remove();\n        }\n        cursors.restore();\n    }\n\n    removeBeacon(beacon) {\n        if (!beacon.isConnected) {\n            return;\n        }\n        this.cleanZwnbsp(beacon);\n        const cursors = this.dependencies.selection.preserveSelection();\n        const parent = beacon.parentElement;\n        cursors.update(callbacksForCursorUpdate.remove(beacon));\n        beacon.remove();\n        cursors.restore();\n        fillEmpty(parent);\n        this.dependencies.format.mergeAdjacentInlines(parent);\n    }\n\n    updateBeacons() {\n        this.commentBeaconManager.sortThreads();\n        this.commentBeaconManager.drawThreadOverlays();\n    }\n\n    destroy() {\n        super.destroy();\n        this.alive = false;\n        registry.category(this.config.localOverlayContainers.key).remove(this.overlayComponentsKey);\n        this.commentBeaconManager.destroy();\n        this.localOverlay.remove();\n    }\n\n    cleanForSave({ root }) {\n        const bogusIds = new Set([\"undefined\"]);\n        for (const beacon of this.commentBeaconManager.bogusBeacons) {\n            bogusIds.add(beacon.dataset.id);\n        }\n        for (const beacon of root.querySelectorAll(\".oe_thread_beacon\")) {\n            if (bogusIds.has(beacon.dataset.id)) {\n                beacon.remove();\n            } else {\n                // remove zwnbsp\n                beacon.replaceChildren();\n                delete beacon.dataset.peerId;\n            }\n        }\n    }\n}\n", "import { Plugin } from \"@html_editor/plugin\";\nimport { isPhrasingContent } from \"@html_editor/utils/dom_info\";\n\nexport class InsertPendingElementPlugin extends Plugin {\n    static id = \"insertPendingElement\";\n    static dependencies = [\"baseContainer\", \"history\", \"dom\", \"selection\"];\n    resources = {\n        start_edition_handlers: this.insertEmbeddedBluePrint.bind(this),\n    };\n\n    insertEmbeddedBluePrint() {\n        const { resModel, resId } = this.config.getRecordInfo();\n        const embeddedBlueprint =\n            this.services.knowledgeCommandsService.popPendingEmbeddedBlueprint({\n                field: \"body\",\n                resId,\n                model: resModel,\n            });\n        if (embeddedBlueprint) {\n            let insert;\n            if (isPhrasingContent(embeddedBlueprint)) {\n                insert = () => {\n                    // insert phrasing content\n                    const paragraph = this.dependencies.baseContainer.createBaseContainer();\n                    paragraph.appendChild(embeddedBlueprint);\n                    this.dependencies.selection.setCursorEnd(this.editable);\n                    this.dependencies.dom.insert(paragraph);\n                    this.dependencies.history.addStep();\n                };\n            } else {\n                insert = () => {\n                    // insert block content\n                    this.dependencies.selection.setCursorEnd(this.editable);\n                    this.dependencies.dom.insert(embeddedBlueprint);\n                    const paragraph = this.dependencies.baseContainer.createBaseContainer();\n                    paragraph.appendChild(document.createElement(\"br\"));\n                    this.dependencies.selection.setCursorEnd(this.editable);\n                    this.dependencies.dom.insert(paragraph);\n                    this.dependencies.history.addStep();\n                };\n            }\n            insert();\n            this.editable.addEventListener(\"onHistoryResetFromPeer\", insert, { once: true });\n        }\n    }\n}\n", "import { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { Composer } from \"@mail/core/common/composer\";\nimport { KnowledgeThread } from \"../../mail/thread/knowledge_thread\";\nimport { scrollTo } from \"@web/core/utils/scrolling\";\nimport { isBrowserChrome } from \"@web/core/browser/feature_detection\";\nimport { KnowledgeCommentsPopover } from \"../comments_popover/comments_popover\";\nimport { KnowledgeCommentCreatorComposer } from \"../../mail/composer/composer\";\nimport { CommentAnchorText } from \"./comment_anchor_text\";\nimport { imageUrl } from \"@web/core/utils/urls\";\nimport {\n    Component,\n    onWillStart,\n    useSubEnv,\n    useRef,\n    useEffect,\n    useState,\n    onWillDestroy,\n} from \"@odoo/owl\";\nimport { effect } from \"@web/core/utils/reactive\";\nimport { batched } from \"@web/core/utils/timing\";\n\nconst DEFAULT_ANCHOR_TEXT_SIZE = 50;\nexport const MIN_THREAD_WIDTH = 300;\n\nexport class KnowledgeCommentsThread extends Component {\n    static components = {\n        CommentAnchorText,\n        Composer,\n        KnowledgeThread,\n        KnowledgeCommentCreatorComposer,\n    };\n    static props = {\n        threadId: { type: String },\n        horizontalDimensions: { type: Object, optional: true },\n        top: { type: Number, optional: true },\n        readonly: { type: Boolean, optional: true },\n    };\n    static template = \"knowledge.KnowledgeCommentsThread\";\n\n    setup() {\n        this.commentsService = useService(\"knowledge.comments\");\n        this.threadScrollableRef = useRef(\"threadScrollableRef\");\n        this.targetRef = useRef(\"targetRef\");\n        this.composerRef = useRef(\"composerRef\");\n        this.commentsState = useState(this.commentsService.getCommentsState());\n        let previousThreadId;\n        this.alive = true;\n        effect(\n            batched((state) => {\n                if (!this.alive) {\n                    return;\n                }\n                if (previousThreadId !== state.activeThreadId) {\n                    if (previousThreadId === this.props.threadId && this.editorThread) {\n                        this.editorThread.onActivate(new CustomEvent(\"knowledge.deactivateThread\"));\n                    }\n                    previousThreadId = state.activeThreadId;\n                }\n            }),\n            [this.commentsState]\n        );\n        onWillDestroy(() => {\n            this.alive = false;\n        });\n        this.state = useState({\n            hasFullAnchorText: false,\n        });\n        useSubEnv({\n            // We need to unset the chatter inside the env of the child Components\n            // because this Object contains values and methods that are linked to the form view's\n            // main chatter. By doing this we distinguish the main chatter from the comments.\n            inChatter: false,\n            chatter: false,\n            closeThread: this.updateResolveState.bind(this, true),\n            inKnowledge: true,\n            isResolved: this.isResolved.bind(this),\n            openThread: this.updateResolveState.bind(this, false),\n        });\n        this.popover = usePopover(KnowledgeCommentsPopover, {\n            closeOnClickAway: true,\n            onClose: () => {\n                this.onClosePopover();\n            },\n            env: this.env,\n            position: \"left-start\",\n            popoverClass: \"o_knowledge_comments_popover\",\n        });\n        const onActivate = (ev) => {\n            switch (ev.type) {\n                case \"click\":\n                    this.activateThread();\n                    break;\n                case \"knowledge.deactivateThread\":\n                    this.focusOut();\n                    break;\n            }\n        };\n        const onFocus = (ev) => {\n            switch (ev.type) {\n                case \"mouseenter\":\n                    this.focusIn();\n                    break;\n                case \"mouseleave\":\n                    this.focusOut();\n                    break;\n            }\n        };\n        useEffect(\n            () => {\n                const editorThread = this.editorThread;\n                if (editorThread) {\n                    editorThread.onActivateMap.set(\"main\", onActivate);\n                    editorThread.onFocusMap.set(\"main\", onFocus);\n                }\n                return () => {\n                    if (editorThread) {\n                        editorThread.onActivateMap.delete(onActivate);\n                        editorThread.onFocusMap.delete(onFocus);\n                    }\n                };\n            },\n            () => [this.editorThread]\n        );\n        let wasActive;\n        useEffect(\n            () => {\n                if (this.isActive && !wasActive && !this.smallUI) {\n                    const scrollable = this.threadScrollableRef.el;\n                    const composer = this.composerRef.el;\n                    if (scrollable && composer) {\n                        const composerRect = composer.getBoundingClientRect();\n                        scrollable.scrollBy({\n                            top: composerRect.height,\n                        });\n                    }\n                }\n                if (this.isActive !== wasActive) {\n                    wasActive = this.isActive;\n                }\n            },\n            () => [this.isActive, this.smallUI]\n        );\n        if (this.commentsState.displayMode === \"handler\") {\n            const setTargetHeight = (target) => {\n                const targetRect = target.getBoundingClientRect();\n                if (!(this.props.threadId in this.env.threadHeights)) {\n                    this.env.threadHeights[this.props.threadId] = {\n                        height: undefined,\n                    };\n                }\n                this.env.threadHeights[this.props.threadId].height = targetRect.height;\n            };\n            useEffect(\n                () => {\n                    if (this.targetRef.el) {\n                        const observer = new ResizeObserver((entries) => {\n                            for (const entry of entries) {\n                                if (entry.target) {\n                                    setTargetHeight(entry.target);\n                                }\n                            }\n                        });\n                        observer.observe(this.targetRef.el);\n                        setTargetHeight(this.targetRef.el);\n                        return () => observer.disconnect();\n                    }\n                },\n                () => [this.targetRef.el]\n            );\n            useEffect(\n                () => {\n                    if (\n                        this.editorThread &&\n                        this.editorThread.threadId === \"undefined\" &&\n                        this.targetRef.el &&\n                        this.isActive &&\n                        this.smallUI\n                    ) {\n                        this.openPopover();\n                    }\n                },\n                () => [this.targetRef.el, this.isActive, this.smallUI, this.editorThread]\n            );\n            useEffect(\n                () => {\n                    if (!this.smallUI && this.popover.isOpen) {\n                        this.popover.close();\n                    }\n                },\n                () => [this.smallUI]\n            );\n        }\n        if (this.props.threadId === \"undefined\") {\n            onWillStart(() => {\n                this.commentsService.loadThreads([this.props.threadId]);\n            });\n        } else {\n            useEffect(\n                () => {\n                    if (!(this.props.threadId in this.commentsState.threadRecords)) {\n                        this.commentsService.loadRecords(this.env.model.root.resId, {\n                            threadId: this.props.threadId,\n                        });\n                    }\n                },\n                () => [this.commentsState.articleId, this.commentsState.displayMode]\n            );\n            useEffect(\n                () => {\n                    if (\n                        this.props.threadId in this.commentsState.threadRecords &&\n                        !(this.props.threadId in this.commentsState.threads)\n                    ) {\n                        this.commentsService.loadThreads([this.props.threadId]);\n                        this.thread.knowledgePreLoading = true;\n                        this.thread.fetchNewMessages().then(() => {\n                            if (this.editorThread?.isProtected()) {\n                                return;\n                            }\n                            let isEmpty = true;\n                            for (const message of this.thread.messages) {\n                                if (message.message_type === \"comment\" && message.body.toString()) {\n                                    isEmpty = false;\n                                    break;\n                                }\n                            }\n                            if (isEmpty) {\n                                this.commentsService.deleteThread(this.props.threadId);\n                            }\n                        });\n                        this.thread.knowledgePreLoading = false;\n                    }\n                },\n                () => [this.threadRecord]\n            );\n        }\n        onWillDestroy(() => {\n            if (this.isActive) {\n                this.commentsState.activeThreadId = undefined;\n            }\n            this.commentsState.focusedThreads.delete(this.props.threadId);\n        });\n    }\n\n    get hasLoaded() {\n        return (\n            this.props.threadId === \"undefined\" ||\n            (this.props.threadId in this.commentsState.threadRecords &&\n                this.props.threadId in this.commentsState.threads)\n        );\n    }\n\n    get hasAllDimensions() {\n        return (\n            this.props.top !== undefined &&\n            this.props.horizontalDimensions !== undefined &&\n            this.props.horizontalDimensions.left !== undefined &&\n            this.props.horizontalDimensions.width !== undefined\n        );\n    }\n\n    get style() {\n        if (this.commentsState.displayMode === \"panel\") {\n            return \"\";\n        }\n        return `\n            position: absolute;\n            top: ${this.props.top}px;\n            left: ${this.props.horizontalDimensions.left}px;\n            width: ${this.props.horizontalDimensions.width}px;\n            transition: top 0.3s, left 0.2s, filter 0.2s;\n            z-index: ${this.isActive ? 1 : \"auto\"};\n            filter: ${\n                !this.smallUI ||\n                this.hasFocus ||\n                (!this.commentsState.activeThreadId && !this.commentsState.focusedThreads.size)\n                    ? \"none\"\n                    : \"grayscale(50%) contrast(50%)\"\n            };\n        `;\n    }\n\n    /**@see EditorThreadInfo */\n    get editorThread() {\n        return this.commentsState.editorThreads[this.props.threadId];\n    }\n\n    get authorUrl() {\n        if (this.thread?.messages?.length) {\n            return this.thread.messages.at(-1).author.avatarUrl;\n        }\n        return imageUrl(\"res.users\", user.userId, \"avatar_128\");\n    }\n\n    get anchorText() {\n        let text = this.fullAnchorText;\n        const brIndex = text.indexOf(\"<br>\");\n        const excludeIndex = brIndex === -1 ? DEFAULT_ANCHOR_TEXT_SIZE : brIndex;\n        if (text.length > excludeIndex) {\n            text = text.substring(0, excludeIndex) + \"...\";\n        }\n        return text;\n    }\n\n    get fullAnchorText() {\n        let text;\n        if (!this.editorThread) {\n            text = this.threadRecord?.article_anchor_text || \"\";\n        } else {\n            text = this.editorThread.anchorText;\n        }\n        return text.replaceAll(\"\\n\", \"<br>\");\n    }\n\n    get hasFocus() {\n        return this.commentsState.hasFocus(this.props.threadId);\n    }\n\n    get isActive() {\n        return this.commentsState.activeThreadId === this.props.threadId;\n    }\n\n    get showReadMore() {\n        const anchorText = this.anchorText;\n        const fullAnchorText = this.fullAnchorText;\n        return fullAnchorText.length > 0 && anchorText.length !== fullAnchorText.length;\n    }\n\n    /**@see Thread */\n    get thread() {\n        return this.commentsState.threads[this.props.threadId];\n    }\n\n    get threadRecord() {\n        return this.commentsState.threadRecords[this.props.threadId];\n    }\n\n    get smallUI() {\n        return (\n            this.commentsState.displayMode === \"handler\" &&\n            this.props.horizontalDimensions.width < MIN_THREAD_WIDTH\n        );\n    }\n\n    activateThread() {\n        this.commentsState.activeThreadId = this.props.threadId;\n        if (this.smallUI) {\n            this.openPopover();\n        }\n    }\n\n    /**\n     * Used for the message actions\n     */\n    isResolved() {\n        if (this.props.threadId === \"undefined\") {\n            return false;\n        }\n        return this.threadRecord.is_resolved;\n    }\n\n    onClick(ev) {\n        if (this.editorThread) {\n            this.editorThread.onActivate(ev);\n        } else {\n            this.activateThread();\n        }\n    }\n\n    focusIn() {\n        this.commentsState.focusedThreads.add(this.props.threadId);\n    }\n\n    focusOut() {\n        this.commentsState.focusedThreads.delete(this.props.threadId);\n    }\n\n    onMouseEnter(ev) {\n        if (this.editorThread) {\n            this.editorThread.onFocus(ev);\n        } else {\n            this.focusIn();\n        }\n    }\n\n    onMouseLeave(ev) {\n        if (this.editorThread) {\n            this.editorThread.onFocus(ev);\n        } else {\n            this.focusOut();\n        }\n    }\n\n    openPopover() {\n        if (!this.popover.isOpen) {\n            const popoverProps = {\n                threadId: this.props.threadId,\n            };\n            if (this.targetRef.el) {\n                this.popover.open(this.targetRef.el, popoverProps);\n            }\n        }\n    }\n\n    onClosePopover() {\n        this.focusOut();\n    }\n\n    showEditorAnchor() {\n        if (!this.editorThread) {\n            return;\n        }\n        if (isBrowserChrome()) {\n            scrollTo(this.editorThread.beaconPair.start, {\n                behavior: \"smooth\",\n            });\n        } else {\n            this.editorThread.beaconPair.start.scrollIntoView({\n                behavior: \"smooth\",\n                block: \"center\",\n            });\n        }\n    }\n\n    async updateResolveState(value) {\n        const changed = await this.commentsService.updateResolveState(this.props.threadId, value);\n        if (changed && this.commentsState.displayMode === \"panel\") {\n            await this.thread.fetchNewMessages();\n        }\n    }\n\n    onCreateThreadCallback(thread) {\n        if (thread) {\n            this.commentsState.editorThreads[thread.id]?.select();\n        }\n    }\n}\n", "import { Component, onWillUpdateProps } from \"@odoo/owl\";\n\nexport class CommentAnchorText extends Component {\n    static template = \"knowledge.CommentAnchorText\";\n    static props = {\n        anchorText: { String },\n    };\n\n    setup() {\n        this.anchorTextArray = this.props.anchorText.split(\"<br>\");\n        onWillUpdateProps((newProps) => {\n            this.anchorTextArray = newProps.anchorText.split(\"<br>\");\n        });\n    }\n}\n", "import { leftPos, rightPos } from \"@html_editor/utils/position\";\nimport { EditorThreadInfo } from \"./editor_thread_info\";\nimport { throttleForAnimation } from \"@web/core/utils/timing\";\nimport { childNodes } from \"@html_editor/utils/dom_traversal\";\n\nfunction binarySearch(comparator, needle, array) {\n    let first = 0;\n    let last = array.length - 1;\n    while (first <= last) {\n        const mid = (first + last) >> 1;\n        const c = comparator(needle, array[mid]);\n        if (c > 0) {\n            first = mid + 1;\n        } else if (c < 0) {\n            last = mid - 1;\n        } else {\n            return mid;\n        }\n    }\n    return first;\n}\n\nexport function compareDOMPosition(a, b) {\n    const compare = a.compareDocumentPosition(b);\n    if (compare & 2) {\n        // b before a\n        return 1;\n    } else if (compare & 4) {\n        // a before b\n        return -1;\n    } else {\n        return 0;\n    }\n}\n\nexport function compareBeaconsThreadIds(a, b) {\n    const id = (beacon) => parseInt(beacon.dataset.id) || -1;\n    const bound = (beacon) => (beacon.dataset.oeType === \"threadBeaconStart\" ? 0 : 1);\n    const idA = id(a);\n    const boundA = bound(a);\n    const idB = id(b);\n    const boundB = bound(b);\n    if (idA > idB) {\n        return 1;\n    } else if (idB > idA) {\n        return -1;\n    } else if (boundA > boundB) {\n        return 1;\n    } else if (boundB > boundA) {\n        return -1;\n    } else {\n        return 0;\n    }\n}\n\nexport class CommentBeaconManager {\n    constructor({\n        commentsState,\n        document,\n        overlayContainer,\n        peerId,\n        readonly = false,\n        source,\n        onStep = () => {},\n        removeBeacon = () => {},\n        setSelection = () => {},\n    } = {}) {\n        this.document = document;\n        this.source = source;\n        this.overlayContainer = overlayContainer;\n        this.beacons = [];\n        this.searchBeaconIndex = binarySearch.bind(undefined, compareDOMPosition);\n        this.beaconsByThreadId = [];\n        this.searchBeaconIndexByThreadId = binarySearch.bind(undefined, compareBeaconsThreadIds);\n        this.bogusBeacons = new Set();\n        this.beaconPairs = {};\n        this.sortedThreadIds = [];\n        this.cleanups = {};\n        this.peerId = peerId;\n        this.readonly = readonly;\n        this.commentsState = commentsState;\n        this.onStep = onStep;\n        this.removeBeacon = removeBeacon;\n        this.setSelection = setSelection;\n        this.drawThreadOverlays = throttleForAnimation(this.drawThreadOverlays.bind(this));\n        this.pendingBeacons = new Set();\n    }\n\n    addBeacons(beacons) {\n        for (const beacon of beacons) {\n            const index = this.searchBeaconIndex(beacon, this.beacons);\n            this.beacons.splice(index, 0, beacon);\n        }\n    }\n\n    deleteBeacons(beacons) {\n        for (const beacon of beacons) {\n            const index = this.searchBeaconIndex(beacon, this.beacons);\n            this.beacons.splice(index, 1);\n        }\n    }\n\n    sortThreads() {\n        this.beacons = [...this.source.querySelectorAll(\".oe_thread_beacon\")];\n        this.beaconsByThreadId = this.beacons.toSorted(compareBeaconsThreadIds);\n        const starts = {};\n        const beaconPairs = {};\n        this.bogusBeacons = new Set();\n        // loop validates that start is before end for beaconPairs\n        for (const beacon of [...this.beacons]) {\n            if (beacon.dataset.oeType === \"threadBeaconStart\") {\n                // Don't consider other peers \"undefined\" beacons\n                if (beacon.dataset.id !== \"undefined\" || beacon.dataset.peerId === this.peerId) {\n                    starts[beacon.dataset.id] = {\n                        start: beacon,\n                    };\n                }\n                this.bogusBeacons.add(beacon);\n            } else if (beacon.dataset.id in starts) {\n                const beaconPair = starts[beacon.dataset.id];\n                delete starts[beacon.dataset.id];\n                beaconPair.end = beacon;\n                if (!this.validate(beaconPair)) {\n                    this.bogusBeacons.add(beaconPair.end);\n                } else {\n                    this.preserveExistingBeaconPair(beaconPair);\n                    this.removeDuplicate(beaconPair.start);\n                    this.removeDuplicate(beaconPair.end);\n                    this.bogusBeacons.delete(beaconPair.start);\n                    beaconPairs[beacon.dataset.id] = beaconPair;\n                }\n            } else {\n                this.bogusBeacons.add(beacon);\n            }\n        }\n        for (const beacon of [...this.bogusBeacons]) {\n            // preserve peer \"undefined\" beacons\n            if (beacon.dataset.id === \"undefined\" && beacon.dataset.peerId !== this.peerId) {\n                this.bogusBeacons.delete(beacon);\n            }\n        }\n        for (const beacon of this.pendingBeacons) {\n            // preserve pending beacons\n            this.bogusBeacons.delete(beacon);\n        }\n        const threadIds = new Set(Object.keys(beaconPairs));\n        for (const threadId of Object.keys(this.beaconPairs)) {\n            if (!threadIds.has(threadId)) {\n                this.cleanupThread(threadId);\n            }\n        }\n        const threadIdsToSort = [];\n        for (const threadId of threadIds) {\n            if (!(threadId in this.beaconPairs)) {\n                this.beaconPairs[threadId] = beaconPairs[threadId];\n            } else {\n                if (this.beaconPairs[threadId].start !== beaconPairs[threadId].start) {\n                    this.beaconPairs[threadId].start = beaconPairs[threadId].start;\n                }\n                if (this.beaconPairs[threadId].end !== beaconPairs[threadId].end) {\n                    this.beaconPairs[threadId].end = beaconPairs[threadId].end;\n                }\n            }\n            let editorThread =\n                this.commentsState.editorThreads[threadId] ||\n                this.commentsState.disabledEditorThreads[threadId];\n            if (!editorThread) {\n                editorThread = new EditorThreadInfo({\n                    beaconPair: this.beaconPairs[threadId],\n                    threadId,\n                    computeTextMap: this.computeTextMap.bind(this),\n                    removeBeaconPair: (beaconPair) => {\n                        this.cleanupThread(beaconPair.start.dataset.id);\n                        this.removeBeacon(beaconPair.start);\n                        this.removeBeacon(beaconPair.end);\n                        this.onStep();\n                    },\n                    setSelectionInBeaconPair: (beaconPair) => {\n                        if (this.validate(beaconPair)) {\n                            const [anchorNode, anchorOffset] = rightPos(beaconPair.start);\n                            this.setSelection({\n                                anchorNode,\n                                anchorOffset,\n                            });\n                        }\n                    },\n                    setBeaconPairId: (beaconPair, id) => {\n                        beaconPair.start.dataset.id = id;\n                        beaconPair.end.dataset.id = id;\n                        this.onStep();\n                    },\n                    enableBeaconPair: (beaconPair) => {\n                        if (this.isDisabled(beaconPair.start)) {\n                            this.enable(beaconPair.start);\n                        }\n                        if (this.isDisabled(beaconPair.end)) {\n                            this.enable(beaconPair.end);\n                        }\n                        const threadId = beaconPair.start.dataset.id;\n                        const editorThread = this.commentsState.disabledEditorThreads[threadId];\n                        if (editorThread) {\n                            this.commentsState.editorThreads[threadId] = editorThread;\n                            delete this.commentsState.disabledEditorThreads[threadId];\n                        }\n                        this.onStep();\n                    },\n                    disableBeaconPair: (beaconPair) => {\n                        if (!this.isDisabled(beaconPair.start)) {\n                            this.disable(beaconPair.start);\n                        }\n                        if (!this.isDisabled(beaconPair.end)) {\n                            this.disable(beaconPair.end);\n                        }\n                        const threadId = beaconPair.start.dataset.id;\n                        const editorThread = this.commentsState.editorThreads[threadId];\n                        if (editorThread) {\n                            this.commentsState.disabledEditorThreads[threadId] = editorThread;\n                            delete this.commentsState.editorThreads[threadId];\n                        }\n                        this.onStep();\n                    },\n                    isOwned: (beaconPair) => {\n                        if (!this.peerId) {\n                            return true;\n                        } else {\n                            const peerId = beaconPair.start.dataset.peerId;\n                            return !peerId || this.peerId === peerId;\n                        }\n                    },\n                });\n            } else {\n                editorThread.beaconPair = this.beaconPairs[threadId];\n            }\n            if (\n                this.isDisabled(this.beaconPairs[threadId].start) ||\n                this.isDisabled(this.beaconPairs[threadId].end)\n            ) {\n                if (!(threadId in this.commentsState.disabledEditorThreads)) {\n                    this.commentsState.disabledEditorThreads[threadId] = editorThread;\n                }\n                if (threadId in this.commentsState.editorThreads) {\n                    delete this.commentsState.editorThreads[threadId];\n                }\n                this.cleanupBeaconPair(threadId);\n            } else {\n                if (!(threadId in this.commentsState.editorThreads)) {\n                    this.commentsState.editorThreads[threadId] = editorThread;\n                }\n                if (threadId in this.commentsState.disabledEditorThreads) {\n                    delete this.commentsState.disabledEditorThreads[threadId];\n                }\n                threadIdsToSort.push(threadId);\n            }\n        }\n        this.sortedThreadIds = threadIdsToSort.sort((a, b) => {\n            return compareDOMPosition(this.beaconPairs[a].start, this.beaconPairs[b].start);\n        });\n    }\n\n    drawThreadOverlays() {\n        const overlayRect = this.overlayContainer.getBoundingClientRect();\n        for (const threadId of this.sortedThreadIds) {\n            this.cleanupBeaconPair(threadId);\n            const beaconPair = this.beaconPairs[threadId];\n            if (!beaconPair.start.isConnected || !beaconPair.end.isConnected) {\n                continue;\n            }\n            const range = new Range();\n            range.setStart(...rightPos(beaconPair.start));\n            range.setEnd(...leftPos(beaconPair.end));\n            const clientRects = Array.from(range.getClientRects());\n            if (!clientRects.length) {\n                continue;\n            }\n            this.commentsState.editorThreads[threadId].top = clientRects[0].y - overlayRect.y;\n            clientRects.reverse();\n            const identifyRect = (big, small) => {\n                if (big.width === 0 || big.height === 0) {\n                    return;\n                }\n                if (\n                    small &&\n                    Math.floor(big.x) <= small.x &&\n                    Math.floor(big.y) <= small.y &&\n                    Math.ceil(big.x + big.width) >= small.x + small.width &&\n                    Math.ceil(big.y + big.height) >= small.y + small.height\n                ) {\n                    return;\n                }\n                // Faster than elementsFromPoint, but some rects will be omitted\n                // if they are under another element like the editor toolbar.\n                const target = this.document.elementFromPoint(\n                    big.x + big.width / 2,\n                    big.y + big.height / 2\n                );\n                if (!target) {\n                    return;\n                }\n                let valid = true;\n                let closestEditable = target.closest(\"[data-embedded-editable]\");\n                if (!this.source.contains(closestEditable)) {\n                    closestEditable = undefined;\n                }\n                let embedded = target.closest(\"[data-embedded]\");\n                if (!this.source.contains(embedded)) {\n                    embedded = undefined;\n                }\n                if (embedded && (!closestEditable || !embedded.contains(closestEditable))) {\n                    valid = false;\n                }\n                if (!valid || (target.textContent === \"\" && target.nodeName !== \"IMG\")) {\n                    return;\n                }\n                return target;\n            };\n            let previousRect;\n            const indicators = [];\n            for (const rect of clientRects) {\n                const identity = identifyRect(rect, previousRect);\n                if (rect.width && rect.height) {\n                    previousRect = rect;\n                }\n                if (!identity || !this.source.contains(identity)) {\n                    continue;\n                }\n                let rectElement;\n                let onFocus;\n                let onActivate;\n                switch (identity.nodeName) {\n                    case \"IMG\":\n                        rectElement = this.createImgOverlay(\n                            rect,\n                            overlayRect,\n                            threadId,\n                            getComputedStyle(identity)\n                        );\n                        onFocus = () => {\n                            const style = getComputedStyle(identity);\n                            rectElement.style.setProperty(\"border-radius\", style.borderRadius);\n                            rectElement.style.setProperty(\n                                \"box-shadow\",\n                                `0 0 0 8px ${this.getThreadOverlayColor(\n                                    this.commentsState.hasFocus(threadId)\n                                )}`\n                            );\n                        };\n                        onActivate = onFocus;\n                        break;\n                    default:\n                        rectElement = this.createTextOverlay(rect, overlayRect, threadId);\n                        onFocus = () => {\n                            rectElement.style.setProperty(\n                                \"background-color\",\n                                this.getThreadOverlayColor(this.commentsState.hasFocus(threadId))\n                            );\n                        };\n                        onActivate = onFocus;\n                        break;\n                }\n                if (rectElement) {\n                    this.setupOverlayEvents({\n                        rectElement,\n                        threadId,\n                        onFocus,\n                        onActivate,\n                    });\n                    indicators.push(rectElement);\n                }\n            }\n            this.overlayContainer.append(...indicators);\n        }\n    }\n\n    createTextOverlay(rect, overlayRect, threadId) {\n        const { x, y, width, height } = rect;\n        const rectElement = this.document.createElement(\"div\");\n        rectElement.style = `\n            position: absolute;\n            top: ${y - overlayRect.y}px;\n            left: ${x - overlayRect.x}px;\n            width: ${width}px;\n            height: ${height}px;\n            pointer-events: ${this.readonly ? \"auto\" : \"none\"};\n            cursor: ${this.readonly ? \"grab\" : \"auto\"};\n            background-color: ${this.getThreadOverlayColor(this.commentsState.hasFocus(threadId))};\n            opacity: 0.5;\n        `;\n        rectElement.dataset.threadId = threadId;\n        return rectElement;\n    }\n\n    createImgOverlay(rect, overlayRect, threadId, style) {\n        const { x, y, width, height } = rect;\n        const rectElement = this.document.createElement(\"div\");\n        rectElement.style = `\n            position: absolute;\n            top: ${y - overlayRect.y}px;\n            left: ${x - overlayRect.x}px;\n            width: ${width}px;\n            height: ${height}px;\n            pointer-events: ${this.readonly ? \"auto\" : \"none\"};\n            cursor: ${this.readonly ? \"grab\" : \"auto\"};\n            box-shadow: 0 0 0 8px ${this.getThreadOverlayColor(\n                this.commentsState.hasFocus(threadId)\n            )};\n            border-radius: ${style.borderRadius};\n            opacity: 0.5;\n        `;\n        rectElement.dataset.threadId = threadId;\n        return rectElement;\n    }\n\n    setupOverlayEvents({ rectElement, threadId, onFocus, onActivate }) {\n        this.cleanups[threadId] ||= new Set();\n        const thread = this.commentsState.editorThreads[threadId];\n        if (onActivate) {\n            thread.onActivateMap.set(rectElement, onActivate);\n        }\n        if (onFocus) {\n            thread.onFocusMap.set(rectElement, onFocus);\n        }\n        const onReadonlyActivate = (ev) => {\n            thread.onActivate(ev);\n        };\n        const onReadonlyFocus = (ev) => {\n            thread.onFocus(ev);\n        };\n        if (this.readonly) {\n            rectElement.addEventListener(\"click\", onReadonlyActivate);\n            rectElement.addEventListener(\"mouseenter\", onReadonlyFocus);\n            rectElement.addEventListener(\"mouseleave\", onReadonlyFocus);\n        }\n        this.cleanups[threadId].add(() => {\n            if (this.readonly) {\n                rectElement.removeEventListener(\"click\", onReadonlyActivate);\n                rectElement.removeEventListener(\"mouseenter\", onReadonlyFocus);\n                rectElement.removeEventListener(\"mouseleave\", onReadonlyFocus);\n            }\n            thread.onActivateMap.delete(rectElement);\n            thread.onFocusMap.delete(rectElement);\n            rectElement.remove();\n        });\n    }\n\n    getThreadOverlayColor(focus) {\n        return `rgba(27, 161, 228, ${focus ? \"0.75\" : \"0.25\"})`;\n    }\n\n    computeTextMap(beaconPair) {\n        const range = new Range();\n        range.setStart(...rightPos(beaconPair.start));\n        range.setEnd(...leftPos(beaconPair.end));\n        const fragment = range.cloneContents();\n        const embeds = [...fragment.querySelectorAll(\"[data-embedded]\")].reverse();\n        for (const embed of embeds) {\n            embed.replaceWith(...embed.querySelectorAll(\"[data-embedded-editable]\"));\n        }\n        return childNodes(fragment).map((node) => {\n            if (node.nodeName === \"IMG\") {\n                return node.src;\n            }\n            return node.textContent.trim();\n        });\n    }\n\n    validate(beaconPair) {\n        // is in DOM\n        if (!beaconPair.start.isConnected || !beaconPair.end.isConnected) {\n            return false;\n        }\n        // start is before end\n        if (compareDOMPosition(beaconPair.start, beaconPair.end) !== -1) {\n            return false;\n        }\n        // is related to the correct article\n        if (\n            parseInt(beaconPair.start.dataset.res_id) !== this.commentsState.articleId ||\n            parseInt(beaconPair.end.dataset.res_id) !== this.commentsState.articleId\n        ) {\n            return false;\n        }\n        // is deleted\n        if (this.commentsState.deletedThreadIds.has(beaconPair.start.dataset.id)) {\n            return false;\n        }\n        if (\n            this.commentsState.activeThreadId !== \"undefined\" &&\n            beaconPair.start.dataset.id === \"undefined\" &&\n            (!this.pendingBeacons.has(beaconPair.start) || !this.pendingBeacons.has(beaconPair.end))\n        ) {\n            return false;\n        }\n        // contains no visible text\n        if (this.computeTextMap(beaconPair).join(\"\").trim() === \"\") {\n            return false;\n        }\n        return true;\n    }\n\n    removeDuplicate(beacon) {\n        // while element at position search compared\n        let index = this.searchBeaconIndexByThreadId(beacon, this.beaconsByThreadId);\n        let currentBeacon = this.beaconsByThreadId.at(index);\n        while (currentBeacon && !compareBeaconsThreadIds(beacon, currentBeacon)) {\n            this.beaconsByThreadId.splice(index, 1);\n            if (beacon !== currentBeacon) {\n                this.bogusBeacons.add(currentBeacon);\n                this.deleteBeacons([currentBeacon]);\n            }\n            index = this.searchBeaconIndexByThreadId(beacon, this.beaconsByThreadId);\n            currentBeacon = this.beaconsByThreadId.at(index);\n        }\n    }\n\n    preserveExistingBeaconPair(beaconPair) {\n        // issue: should remove duplicates even if there is no concurrentBeaconPair\n        // already exists and is valid elsewhere => keep existing\n        const concurrentBeaconPair = this.beaconPairs[beaconPair.start.dataset.id];\n        if (\n            concurrentBeaconPair &&\n            beaconPair !== concurrentBeaconPair &&\n            this.validate(concurrentBeaconPair) &&\n            compareDOMPosition(concurrentBeaconPair.start, concurrentBeaconPair.end) === -1\n        ) {\n            if (beaconPair.start !== concurrentBeaconPair.start) {\n                beaconPair.start = concurrentBeaconPair.start;\n            }\n            if (beaconPair.end !== concurrentBeaconPair.end) {\n                beaconPair.end = concurrentBeaconPair.end;\n            }\n        }\n    }\n\n    activateRelatedThread(target) {\n        this.sortThreads();\n        const index = this.searchBeaconIndex(target, this.beacons);\n        const ends = {};\n        let threadId;\n        for (let i = index - 1; i >= 0; i--) {\n            const beacon = this.beacons[i];\n            if (beacon.dataset.oeType === \"threadBeaconEnd\") {\n                ends[beacon.dataset.id] = {\n                    end: beacon,\n                };\n            } else if (\n                beacon.dataset.id in ends ||\n                !(beacon.dataset.id in this.beaconPairs) ||\n                !this.beaconPairs[beacon.dataset.id].end.isConnected\n            ) {\n                continue;\n            } else if (beacon.dataset.id in this.commentsState.editorThreads) {\n                threadId = beacon.dataset.id;\n                break;\n            }\n        }\n        this.commentsState.activeThreadId = threadId;\n        this.drawThreadOverlays();\n    }\n\n    isDisabled(beacon) {\n        return beacon.classList.contains(\"oe_disabled_thread_beacon\");\n    }\n\n    enable(beacon) {\n        beacon.classList.remove(\"oe_disabled_thread_beacon\");\n    }\n\n    disable(beacon) {\n        beacon.classList.add(\"oe_disabled_thread_beacon\");\n    }\n\n    removeBogusBeacons() {\n        for (const beacon of this.bogusBeacons) {\n            // TODO ABD: evaluate cleanupThread ?\n            this.cleanupBeaconPair(beacon.dataset.id);\n            this.removeBeacon(beacon);\n        }\n        this.bogusBeacons = new Set();\n    }\n\n    cleanupBeaconPair(threadId) {\n        for (const cleanup of this.cleanups[threadId] || []) {\n            cleanup();\n        }\n        delete this.cleanups[threadId];\n    }\n\n    destroy() {\n        for (const threadId of Object.keys(this.cleanups)) {\n            this.cleanupThread(threadId);\n        }\n        for (const threadId of Object.keys(this.commentsState.editorThreads)) {\n            delete this.commentsState.editorThreads[threadId];\n        }\n        for (const threadId of Object.keys(this.commentsState.disabledEditorThreads)) {\n            delete this.commentsState.disabledEditorThreads[threadId];\n        }\n    }\n\n    cleanupThread(threadId) {\n        this.cleanupBeaconPair(threadId);\n        delete this.beaconPairs[threadId];\n        delete this.commentsState.editorThreads[threadId];\n        delete this.commentsState.disabledEditorThreads[threadId];\n    }\n}\n", "import { MIN_THREAD_WIDTH, KnowledgeCommentsThread } from \"../comment/comment\";\nimport { useCallbackRecorder } from \"@web/search/action_hook\";\nimport { batched, Component, reactive, useEffect, useState, useSubEnv } from \"@odoo/owl\";\nimport { CommentBeaconManager } from \"../../comments/comment_beacon_manager\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { localization } from \"@web/core/l10n/localization\";\n\nconst MAX_THREAD_WIDTH = 400;\nconst SMALL_THREAD_WIDTH = 40; // o_knowledge_small_ui\n\nexport class KnowledgeCommentsHandler extends Component {\n    static template = \"knowledge.KnowledgeCommentsHandler\";\n    static components = { KnowledgeCommentsThread };\n    static props = {\n        commentBeaconManager: { type: CommentBeaconManager },\n        contentRef: { type: Object },\n    };\n\n    setup() {\n        this.commentsService = useService(\"knowledge.comments\");\n        this.commentsState = useState(this.commentsService.getCommentsState());\n        const debouncedThreadDimensions = debounce(() => {\n            this.computeThreadDimensions();\n        }, 300);\n        const batchedComputeVerticalDimensions = batched(this.computeVerticalDimensions.bind(this));\n        useSubEnv({\n            threadHeights: reactive({}, batchedComputeVerticalDimensions),\n        });\n        this.lastActiveThreadId;\n        this.state = useState({\n            threadDimensions: {\n                horizontal: {},\n                threadTops: {},\n            },\n        });\n        useCallbackRecorder(this.env.__onLayoutGeometryChange__, debouncedThreadDimensions);\n        let activeThreadId;\n        useEffect(\n            () => {\n                if (this.commentsState.activeThreadId !== activeThreadId) {\n                    activeThreadId = this.commentsState.activeThreadId;\n                    batchedComputeVerticalDimensions();\n                }\n            },\n            () => [this.commentsState.activeThreadId]\n        );\n        useEffect(\n            () => {\n                const editorThreads = Object.keys(this.commentsState.editorThreads);\n                if (\n                    editorThreads.some((threadId) => {\n                        return this.props.commentBeaconManager.sortedThreadIds.includes(threadId);\n                    })\n                ) {\n                    debouncedThreadDimensions();\n                }\n            },\n            () => [\n                this.commentsState.editorThreads,\n                Object.keys(this.commentsState.editorThreads).toString(),\n            ]\n        );\n    }\n\n    get mainPositionThreadId() {\n        if (\n            this.commentsState.activeThreadId &&\n            this.commentsState.activeThreadId !== this.lastActiveThreadId\n        ) {\n            this.lastActiveThreadId = this.commentsState.activeThreadId;\n        } else if (\n            !this.props.commentBeaconManager.sortedThreadIds.includes(this.lastActiveThreadId)\n        ) {\n            this.lastActiveThreadId = undefined;\n        }\n        return this.lastActiveThreadId || this.props.commentBeaconManager.sortedThreadIds.at(0);\n    }\n\n    computeHorizontalDimensions() {\n        if (!this.props.contentRef.el) {\n            return;\n        }\n        const rtl = localization.direction === \"rtl\";\n        const keys = {\n            paddingRight: \"paddingRight\",\n            left: \"left\",\n            right: \"right\",\n        };\n        if (rtl) {\n            Object.assign(keys, {\n                paddingRight: \"paddingLeft\",\n                left: \"right\",\n                right: \"left\",\n            });\n        }\n        const contentStyle = getComputedStyle(this.props.contentRef.el);\n        const paddingRight = parseInt(contentStyle[keys.paddingRight]) || 0;\n\n        // Manually calculate margin to circumvent zero-margin bug in chromium-based browsers\n        const contentRect = this.props.contentRef.el.getBoundingClientRect();\n        const parentRect = this.props.contentRef.el.parentElement.getBoundingClientRect();\n        const marginRight = Math.abs(parentRect[keys.right] - contentRect[keys.right]);\n        const marginLeft = Math.abs(parentRect[keys.left] - contentRect[keys.left]);\n\n        const availableWidth = Math.max(0, Math.floor(marginRight + paddingRight));\n        let width = Math.min(MAX_THREAD_WIDTH, Math.max(0, availableWidth - 20));\n        if (!width) {\n            return;\n        }\n        if (width < MIN_THREAD_WIDTH) {\n            width = SMALL_THREAD_WIDTH;\n        }\n        const left =\n            (rtl ? -1 : 1) *\n            Math.ceil(\n                marginLeft +\n                    contentRect.width -\n                    paddingRight +\n                    (availableWidth + (rtl ? 1 : -1) * width) / 2\n            );\n        this.state.threadDimensions.horizontal = { left, width };\n    }\n\n    computeVerticalDimensions() {\n        const activeId = this.mainPositionThreadId;\n        if (!activeId || this.commentsState.editorThreads[activeId]?.top === undefined) {\n            return;\n        }\n        const threadIds = this.props.commentBeaconManager.sortedThreadIds.filter((threadId) => {\n            return (\n                threadId in this.commentsState.editorThreads &&\n                this.commentsState.editorThreads[threadId].top\n            );\n        });\n        const index = threadIds.indexOf(activeId);\n        this.setThreadTop(activeId, this.commentsState.editorThreads[activeId].top);\n        let masterTop = this.getThreadTop(activeId);\n        for (let i = index - 1; i >= 0; i--) {\n            const threadId = threadIds[i];\n            const expectedTop = this.commentsState.editorThreads[threadId].top;\n            const height = this.env.threadHeights[threadId]?.height || 0;\n            if (expectedTop + height < masterTop) {\n                masterTop = expectedTop;\n            } else {\n                masterTop -= height;\n            }\n            this.setThreadTop(threadId, masterTop);\n        }\n        masterTop = this.getThreadTop(activeId) + (this.env.threadHeights[activeId]?.height || 0);\n        for (let i = index + 1; i < threadIds.length; i++) {\n            const threadId = threadIds[i];\n            const expectedTop = this.commentsState.editorThreads[threadId].top;\n            masterTop = Math.max(masterTop, expectedTop);\n            this.setThreadTop(threadId, masterTop);\n            masterTop += this.env.threadHeights[threadId]?.height || 0;\n        }\n    }\n\n    computeThreadDimensions() {\n        this.computeHorizontalDimensions();\n        this.computeVerticalDimensions();\n    }\n\n    setThreadTop(threadId, top) {\n        if (!(threadId in this.state.threadDimensions.threadTops)) {\n            this.state.threadDimensions.threadTops[threadId] = {\n                top: undefined,\n            };\n        }\n        this.state.threadDimensions.threadTops[threadId].top = top;\n    }\n\n    getThreadTop(threadId) {\n        return this.state.threadDimensions.threadTops[threadId]?.top;\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { KnowledgeCommentsThread } from \"../comment/comment\";\nimport { Component, onWillDestroy, onWillStart, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { batched, debounce } from \"@web/core/utils/timing\";\nimport { effect } from \"@web/core/utils/reactive\";\nimport { LOAD_THREADS_LIMIT } from \"../../comments/comments_service\";\n\nexport class KnowledgeCommentsPanel extends Component {\n    static template = \"knowledge.KnowledgeCommentsPanel\";\n    static components = { KnowledgeCommentsThread };\n    static props = { ...standardWidgetProps };\n\n    setup() {\n        this.rootRef = useRef(\"root\");\n        this.commentsService = useService(\"knowledge.comments\");\n        this.commentsState = useState(this.commentsService.getCommentsState());\n        let threadRecordsKeys;\n        this.alive = true;\n        effect(\n            batched((state) => {\n                if (!this.alive) {\n                    return;\n                }\n                if (state.displayMode !== \"panel\") {\n                    return;\n                }\n                const threadRecords = state.threadRecords;\n                const threadIds = Object.keys(threadRecords);\n                const keys = threadIds.toString();\n                if (keys !== threadRecordsKeys) {\n                    this.computeThreadIds();\n                    threadRecordsKeys = keys;\n                }\n            }),\n            [this.commentsState]\n        );\n        onWillDestroy(() => {\n            this.alive = false;\n        });\n        this.state = useState({\n            mode: \"unresolved\", // \"resolved\" / \"all\"\n            loadMoreResolved: false,\n            loadMoreOpen: false,\n            loading: false,\n            threadIds: [],\n        });\n        let firstLoad = true;\n        useEffect(\n            () => {\n                if (this.commentsState.displayMode !== \"panel\") {\n                    return;\n                }\n                this.computeThreadIds();\n                this.commentsService\n                    .loadRecords(this.env.model.root.resId, {\n                        ignoreBatch: true,\n                        includeLoaded: true,\n                        domain: this.domain,\n                    })\n                    .then((count) => {\n                        if (firstLoad) {\n                            firstLoad = false;\n                            if (count !== undefined && count < LOAD_THREADS_LIMIT) {\n                                this.sealLoadMoreState();\n                            } else {\n                                this.state.loadMoreOpen = true;\n                                this.state.loadMoreResolved = true;\n                            }\n                        }\n                    });\n            },\n            () => [this.state.mode, this.commentsState.articleId, this.commentsState.displayMode]\n        );\n        // TODO ABD: refactor form view style to not have to do this\n        useEffect(\n            () => {\n                if (this.commentsState.displayMode === \"panel\") {\n                    this.rootRef.el?.parentElement.classList.remove(\"d-none\");\n                } else {\n                    this.rootRef.el?.parentElement.classList.add(\"d-none\");\n                }\n            },\n            () => [this.commentsState.displayMode]\n        );\n        onWillStart(async () => {\n            // TODO ABD: test this use case\n            if (\n                this.env.services.action.currentController?.action?.context?.show_resolved_threads\n            ) {\n                this.commentsState.displayMode = \"panel\";\n                this.mode = \"resolved\";\n                this.env.services.action.currentController.action.context.show_resolved_threads = false;\n            }\n            this.isPortalUser = await user.hasGroup(\"base.group_portal\");\n            this.isInternalUser = await user.hasGroup(\"base.group_user\");\n        });\n        const loadMore = debounce(this.loadMore.bind(this), 500);\n        this.loadMore = () => {\n            this.state.loading = true;\n            loadMore();\n        };\n    }\n\n    canDisplayRecord(threadId) {\n        if (!this.commentsState.threadRecords[threadId]) {\n            return true;\n        }\n        if (this.state.mode === \"unresolved\") {\n            return !this.commentsState.threadRecords[threadId].is_resolved;\n        } else if (this.state.mode === \"resolved\") {\n            return this.commentsState.threadRecords[threadId].is_resolved;\n        }\n        return true;\n    }\n\n    get couldLoadMore() {\n        if (this.state.mode === \"unresolved\") {\n            return this.state.loadMoreOpen;\n        } else if (this.state.mode === \"resolved\") {\n            return this.state.loadMoreResolved;\n        } else {\n            return this.state.loadMoreOpen || this.state.loadMoreResolved;\n        }\n    }\n\n    get domain() {\n        let domain = undefined;\n        if (this.state.mode === \"unresolved\") {\n            domain = [[\"is_resolved\", \"=\", false]];\n        } else if (this.state.mode === \"resolved\") {\n            domain = [[\"is_resolved\", \"=\", true]];\n        }\n        return domain;\n    }\n\n    computeThreadIds() {\n        const threadIds = [];\n        for (const [threadId, record] of Object.entries(this.commentsState.threadRecords)) {\n            if (\n                this.state.mode === \"all\" ||\n                (this.state.mode === \"resolved\" && record.is_resolved) ||\n                (this.state.mode === \"unresolved\" && !record.is_resolved)\n            ) {\n                threadIds.push(threadId);\n            }\n        }\n        this.state.threadIds = threadIds.sort((threadIdA, threadIdB) => {\n            const dateA = this.commentsState.threadRecords[threadIdA].write_date;\n            const dateB = this.commentsState.threadRecords[threadIdB].write_date;\n            if (dateA < dateB) {\n                return 1;\n            } else if (dateA > dateB) {\n                return -1;\n            } else {\n                return 0;\n            }\n        });\n    }\n\n    onChangeMode(ev) {\n        this.state.mode = ev.target.value;\n    }\n\n    async loadMore() {\n        const count = await this.commentsService.loadRecords(this.env.model.root.resId, {\n            ignoreBatch: true,\n            domain: this.domain,\n        });\n        if (count !== undefined && count < LOAD_THREADS_LIMIT) {\n            this.sealLoadMoreState();\n        }\n        this.state.loading = false;\n    }\n\n    sealLoadMoreState() {\n        if (this.state.mode === \"unresolved\") {\n            this.state.loadMoreOpen = false;\n        } else if (this.state.mode === \"resolved\") {\n            this.state.loadMoreResolved = false;\n        } else {\n            this.state.loadMoreResolved = false;\n            this.state.loadMoreOpen = false;\n        }\n    }\n}\n\nexport const knowledgeCommentsPanel = {\n    component: KnowledgeCommentsPanel,\n    additionalClasses: [\"d-none\", \"col-12\", \"col-lg-4\", \"border-start\", \"d-print-none\"],\n};\n\nregistry.category(\"view_widgets\").add(\"knowledge_comments_panel\", knowledgeCommentsPanel);\n", "import { Composer } from \"@mail/core/common/composer\";\nimport { KnowledgeCommentCreatorComposer } from \"../../mail/composer/composer\";\nimport { KnowledgeThread } from \"@knowledge/mail/thread/knowledge_thread\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, useRef, useState } from \"@odoo/owl\";\n\nexport class KnowledgeCommentsPopover extends Component {\n    static template = \"knowledge.KnowledgeCommentsPopover\";\n    static props = {\n        threadId: { type: String },\n        close: { type: Function },\n    };\n    static components = { Composer, KnowledgeThread, KnowledgeCommentCreatorComposer };\n\n    setup() {\n        this.commentsService = useService(\"knowledge.comments\");\n        this.commentsState = useState(this.commentsService.getCommentsState());\n        this.rootRef = useRef(\"rootRef\");\n    }\n\n    onPostCallback() {\n        this.props.close();\n    }\n\n    onCreateThreadCallback(thread) {\n        if (thread) {\n            this.commentsState.editorThreads[thread.id]?.select();\n        }\n    }\n\n    get thread() {\n        return this.commentsState.threads[this.props.threadId];\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { batched, reactive } from \"@odoo/owl\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { uuid } from \"@web/views/utils\";\nimport { effect } from \"@web/core/utils/reactive\";\n\nconst ARTICLE_THREAD_FIELDS = [\n    \"id\",\n    \"article_id\",\n    \"article_anchor_text\",\n    \"is_resolved\",\n    \"write_date\",\n];\n\nexport const LOAD_THREADS_LIMIT = 30;\n\nfunction clearThreadComposer(composer) {\n    if (!composer) {\n        return;\n    }\n    composer.clear();\n    browser.localStorage.removeItem(composer.localId);\n}\n\nexport const knowledgeCommentsService = {\n    dependencies: [\"orm\", \"mail.store\"],\n    start(env, services) {\n        this.services = services;\n        this.commentsState = reactive({\n            articleId: undefined,\n            activeThreadId: undefined,\n            // database records\n            threadRecords: {},\n            // mail.store instances\n            threads: {},\n            disabledEditorThreads: {},\n            // editor metadata\n            editorThreads: {},\n            displayMode: \"handler\", // 'handler' 'panel'\n            focusedThreads: new Set(),\n            deletedThreadIds: new Set(),\n            hasFocus(threadId) {\n                return this.focusedThreads.has(threadId) || this.activeThreadId === threadId;\n            },\n        });\n        let previousArticleId;\n        effect(\n            (state) => {\n                if (previousArticleId !== state.articleId) {\n                    this.resetForArticleId();\n                    previousArticleId = state.articleId;\n                }\n            },\n            [this.commentsState]\n        );\n        return {\n            createThread: this.createThread.bind(this),\n            createVirtualThread: this.createVirtualThread.bind(this),\n            deleteThread: this.deleteThread.bind(this),\n            fetchMessages: this.fetchMessages.bind(this),\n            getCommentsState: this.getCommentsState.bind(this),\n            loadRecords: this.loadRecords.bind(this),\n            loadThreads: this.loadThreads.bind(this),\n            setArticleId: this.setArticleId.bind(this),\n            updateResolveState: this.updateResolveState.bind(this),\n        };\n    },\n    async createThread(value, postData) {\n        if (!value || !(\"undefined\" in this.commentsState.editorThreads)) {\n            return;\n        }\n        const loadingId = this.loadingId;\n        const record = await rpc(\"/knowledge/thread/create\", {\n            article_id: this.commentsState.articleId,\n            article_anchor_text: this.commentsState.editorThreads[\"undefined\"].anchorText,\n            fields: ARTICLE_THREAD_FIELDS,\n        });\n        if (loadingId !== this.loadingId) {\n            return;\n        }\n        this.commentsState.threadRecords[record.id] = record;\n        this.commentsState.editorThreads[record.id] = this.commentsState.editorThreads[\"undefined\"];\n        delete this.commentsState.editorThreads[\"undefined\"];\n        this.commentsState.editorThreads[record.id].setThreadId(record.id);\n        clearThreadComposer(this.commentsState.threads[\"undefined\"].composer);\n        const thread = this.services[\"mail.store\"].Thread.insert({\n            id: record.id,\n            model: \"knowledge.article.thread\",\n            articleId: this.commentsState.articleId,\n        });\n        this.commentsState.threads[record.id] = thread;\n        thread.post(value, postData);\n        return thread;\n    },\n    createVirtualThread() {\n        this.commentsState.threads[\"undefined\"] = this.services[\"mail.store\"].Thread.insert({\n            id: undefined,\n            model: \"knowledge.article.thread\",\n            articleId: this.commentsState.articleId,\n        });\n        clearThreadComposer(this.commentsState.threads[\"undefined\"].composer);\n        return this.commentsState.threads[\"undefined\"];\n    },\n    async deleteThread(resId) {\n        if (resId === \"undefined\") {\n            return;\n        }\n        this.batchedDeleteThread.threadIds.add(resId);\n        this.batchedDeleteThread();\n    },\n    // threadId is a number\n    async fetchMessages(threadId) {\n        const deferred = new Deferred();\n        this.batchedFetchMessages.deferredPromises[threadId] ||= [];\n        this.batchedFetchMessages.deferredPromises[threadId].push(deferred);\n        this.batchedFetchMessages.threadIds.add(threadId);\n        this.batchedFetchMessages();\n        return await deferred;\n    },\n    getCommentsState() {\n        return this.commentsState;\n    },\n    loadRecords(articleId, { domain, ignoreBatch, includeLoaded, limit, threadId } = {}) {\n        if (this.commentsState.articleId !== articleId) {\n            return;\n        }\n        if (ignoreBatch) {\n            return this._loadRecords({\n                domain,\n                limit,\n                includeLoaded,\n            });\n        }\n        if (threadId) {\n            if (threadId === \"undefined\") {\n                return;\n            }\n            this.batchedLoadRecords.threadIds.add(threadId);\n            this.batchedLoadRecords();\n        }\n    },\n    loadThreads(resIds) {\n        for (const resId of resIds) {\n            this.commentsState.threads[resId] = this.services[\"mail.store\"].Thread.insert({\n                id: resId === \"undefined\" ? undefined : resId,\n                model: \"knowledge.article.thread\",\n                articleId: this.commentsState.articleId,\n            });\n        }\n    },\n    makeBatchedDeleteThread() {\n        const deleteThread = async () => {\n            if (this.loadingId !== batch.loadingId) {\n                return;\n            }\n            const resIds = [];\n            for (const resId of batch.threadIds) {\n                resIds.push(parseInt(resId));\n            }\n            batch.threadIds = new Set();\n            try {\n                await this.services[\"orm\"].unlink(\"knowledge.article.thread\", resIds);\n            } catch {\n                // deleted threads may already be deleted, or the current user\n                // may not have the right to delete them.\n            }\n            if (this.loadingId !== batch.loadingId) {\n                return;\n            }\n            for (const resId of resIds) {\n                delete this.commentsState.threadRecords[resId];\n                delete this.commentsState.threads[resId];\n                this.commentsState.deletedThreadIds.add(resId.toString());\n                this.commentsState.editorThreads[resId]?.removeBeacons();\n            }\n        };\n        const batch = batched(deleteThread);\n        batch.threadIds = new Set();\n        batch.loadingId = this.loadingId;\n        return batch;\n    },\n    makeBatchedFetchMessages() {\n        const fetchMessages = async () => {\n            if (this.loadingId !== batch.loadingId) {\n                return;\n            }\n            const deferredPromises = batch.deferredPromises;\n            const thread_ids = Array.from(batch.threadIds);\n            batch.threadIds = new Set();\n            batch.deferredPromises = {};\n            let error;\n            let result;\n            try {\n                result = await rpc(\"/knowledge/threads/messages\", {\n                    thread_model: \"knowledge.article.thread\",\n                    thread_ids,\n                });\n            } catch (e) {\n                error = e;\n            }\n            // thread_id is a number, not a string (used for backend)\n            for (const thread_id in deferredPromises) {\n                for (const deferred of deferredPromises[thread_id]) {\n                    if (error) {\n                        deferred.reject(error);\n                    } else {\n                        deferred.resolve(result[thread_id]);\n                    }\n                }\n            }\n        };\n        const batch = batched(fetchMessages);\n        batch.deferredPromises = {};\n        batch.threadIds = new Set();\n        batch.loadingId = this.loadingId;\n        return batch;\n    },\n    makeBatchedLoadRecords() {\n        const loadRecords = async () => {\n            if (this.loadingId !== batch.loadingId) {\n                return;\n            }\n            const excludedSet = new Set(Object.keys(this.commentsState.threadRecords));\n            const targetedSet = batch.threadIds;\n            batch.threadIds = new Set();\n            for (const threadId of [...targetedSet]) {\n                if (excludedSet.has(threadId)) {\n                    targetedSet.delete(threadId);\n                }\n            }\n            let threadRecords = [];\n            if (targetedSet.size) {\n                const queryDomain = [\n                    [\"article_id\", \"=\", this.commentsState.articleId],\n                    [\"id\", \"in\", [...targetedSet]],\n                ];\n                threadRecords = await this.services[\"orm\"].searchRead(\n                    \"knowledge.article.thread\",\n                    queryDomain,\n                    ARTICLE_THREAD_FIELDS\n                );\n                if (this.loadingId !== batch.loadingId) {\n                    return;\n                }\n                for (const threadRecord of threadRecords) {\n                    const threadId = threadRecord.id.toString();\n                    this.commentsState.threadRecords[threadId] = threadRecord;\n                    if (threadRecord.is_resolved) {\n                        this.commentsState.editorThreads[threadId]?.disableBeacons();\n                    } else {\n                        this.commentsState.disabledEditorThreads[threadId]?.enableBeacons();\n                    }\n                }\n            }\n            if (targetedSet.size) {\n                // cleanup targetedThreadIds which do not exist anymore\n                for (const threadId of targetedSet) {\n                    if (\n                        !(threadId in this.commentsState.threadRecords) &&\n                        !this.commentsState.editorThreads[threadId]?.isProtected()\n                    ) {\n                        this.batchedDeleteThread.threadIds.add(threadId);\n                    }\n                }\n                if (this.batchedDeleteThread.threadIds.size) {\n                    this.batchedDeleteThread();\n                }\n            }\n        };\n        const batch = batched(loadRecords);\n        batch.loadingId = this.loadingId;\n        batch.threadIds = new Set();\n        return batch;\n    },\n    makeLoadRecords() {\n        const loadRecords = async ({ domain, limit, includeLoaded } = {}) => {\n            if (!limit) {\n                limit = LOAD_THREADS_LIMIT;\n            }\n            const options = {\n                limit,\n            };\n            const queryDomain = [[\"article_id\", \"=\", this.commentsState.articleId]];\n            if (domain) {\n                queryDomain.push(...domain);\n            }\n            if (!includeLoaded) {\n                const excludedThreadIds = Object.keys(this.commentsState.threadRecords);\n                queryDomain.push([\"id\", \"not in\", excludedThreadIds]);\n            }\n            const threadRecords = await this.services[\"orm\"].searchRead(\n                \"knowledge.article.thread\",\n                queryDomain,\n                ARTICLE_THREAD_FIELDS,\n                options\n            );\n            if (this.loadingId !== loadRecords.loadingId) {\n                return;\n            }\n            for (const threadRecord of threadRecords) {\n                const threadId = threadRecord.id.toString();\n                this.commentsState.threadRecords[threadId] = threadRecord;\n                if (threadRecord.is_resolved) {\n                    this.commentsState.editorThreads[threadId]?.disableBeacons();\n                } else {\n                    this.commentsState.disabledEditorThreads[threadId]?.enableBeacons();\n                }\n            }\n            return threadRecords.length;\n        };\n        loadRecords.loadingId = this.loadingId;\n        return loadRecords;\n    },\n    resetForArticleId() {\n        this.loadingId = uuid();\n        this.commentsState.activeThreadId = undefined;\n        this.commentsState.focusedThreads = new Set();\n        this.commentsState.threadRecords = {};\n        this.commentsState.threads = {};\n        this.commentsState.disabledEditorThreads = {};\n        this.commentsState.editorThreads = {};\n        this.batchedFetchMessages = this.makeBatchedFetchMessages();\n        this.batchedDeleteThread = this.makeBatchedDeleteThread();\n        this.batchedLoadRecords = this.makeBatchedLoadRecords();\n        this._loadRecords = this.makeLoadRecords();\n    },\n    setArticleId(articleId) {\n        if (articleId !== this.commentsState.articleId) {\n            this.commentsState.articleId = articleId;\n            this.resetForArticleId();\n        }\n    },\n    async updateResolveState(resId, resolvedState) {\n        const loadingId = this.loadingId;\n        try {\n            await this.services[\"orm\"].write(\"knowledge.article.thread\", [parseInt(resId)], {\n                is_resolved: resolvedState,\n            });\n        } catch {\n            return false;\n        }\n        if (this.loadingId !== loadingId) {\n            return false;\n        }\n        this.commentsState.threadRecords[resId].is_resolved = resolvedState;\n        if (resolvedState) {\n            this.commentsState.editorThreads[resId]?.disableBeacons();\n        } else {\n            this.commentsState.disabledEditorThreads[resId]?.enableBeacons();\n        }\n        return true;\n    },\n};\n\nregistry.category(\"services\").add(\"knowledge.comments\", knowledgeCommentsService);\n", "export class EditorThreadInfo {\n    constructor({\n        beaconPair,\n        threadId,\n        computeTextMap = () => [],\n        enableBeaconPair = () => {},\n        disableBeaconPair = () => {},\n        isOwned = () => {},\n        removeBeaconPair = () => {},\n        setSelectionInBeaconPair = () => {},\n        setBeaconPairId = () => {},\n    } = {}) {\n        this.beaconPair = beaconPair;\n        this.threadId = threadId;\n        this.computeTextMap = computeTextMap;\n        this.isOwned = isOwned;\n        this.removeBeaconPair = removeBeaconPair;\n        this.setSelectionInBeaconPair = setSelectionInBeaconPair;\n        this.setBeaconPairId = setBeaconPairId;\n        this.enableBeaconPair = enableBeaconPair;\n        this.disableBeaconPair = disableBeaconPair;\n        this.onActivateMap = new Map();\n        this.onFocusMap = new Map();\n        this.hover = false;\n        this.top = 0;\n        this.anchorText = this.computeCurrentAnchorText();\n    }\n\n    computeCurrentAnchorText() {\n        return this.computeTextMap(this.beaconPair).join(\"\\n\").trim();\n    }\n\n    setThreadId(id) {\n        this.threadId = id;\n        this.setBeaconPairId(this.beaconPair, id);\n    }\n\n    enableBeacons() {\n        this.enableBeaconPair(this.beaconPair);\n    }\n\n    disableBeacons() {\n        this.disableBeaconPair(this.beaconPair);\n    }\n\n    removeBeacons() {\n        this.removeBeaconPair(this.beaconPair);\n    }\n\n    select() {\n        this.setSelectionInBeaconPair(this.beaconPair);\n    }\n\n    handleEventMapEntries(ev, map) {\n        map.get(\"main\")?.(ev);\n        for (const [key, handler] of map.entries()) {\n            if (key.isConnected) {\n                handler(ev);\n            } else if (key !== \"main\") {\n                map.delete(key);\n            }\n        }\n    }\n\n    isProtected() {\n        return !this.isOwned(this.beaconPair);\n    }\n\n    onActivate(ev) {\n        this.handleEventMapEntries(ev, this.onActivateMap);\n    }\n\n    onFocus(ev) {\n        this.handleEventMapEntries(ev, this.onFocusMap);\n    }\n}\n", "import { Composer } from \"@mail/core/common/composer\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { onWillDestroy } from \"@odoo/owl\";\n\n/**\n * This Component is an extension of the classic Composer used inside the chatter. It is called when a\n * user is just creating a new comment => when we set the id of the Thread to undefined.\n * This enables us to limit the creation of void comments inside the DB and lessen journal entries in\n * it.\n * After comment creation, this component is destroyed and is replaced with the regular Composer.\n */\nexport class KnowledgeCommentCreatorComposer extends Composer {\n    static props = [...Composer.props, \"onCreateThreadCallback?\"];\n\n    setup() {\n        super.setup();\n        this.commentsService = useService(\"knowledge.comments\");\n        this.newThread = undefined;\n        onWillDestroy(() => {\n            this.props.onCreateThreadCallback?.(this.newThread);\n        });\n    }\n\n    /**\n     * @override\n     */\n    async _sendMessage(value, postData) {\n        this.newThread = await this.commentsService.createThread(value, postData);\n        this.clear();\n    }\n}\n", "import { Composer } from \"@mail/core/common/composer\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\n// TODO ABD: could be replaced by a KnowledgeComposer ?\n/** @type {Composer} */\nconst composerPatch = {\n    get hasGifPicker() {\n        // Done to remove the gif picker when in Knowledge as per the specs\n        return super.hasGifPicker && !this.env.inKnowledge;\n    },\n};\npatch(Composer.prototype, composerPatch);\n", "/* @odoo-module */\n\nimport { EmojiPicker } from \"@web/core/emoji_picker/emoji_picker\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\nEmojiPicker.props.push(\"hasRemoveFeature?\");\n\npatch(EmojiPicker.prototype, {\n    removeEmoji() {\n        this.props.onSelect(false);\n        this.gridRef.el.scrollTop = 0;\n        this.props.close?.();\n        this.props.onClose?.();\n    },\n});\n", "import { Message } from \"@mail/core/common/message\";\nimport { user } from \"@web/core/user\";\nimport { onWillStart } from \"@odoo/owl\";\n\nexport class KnowledgeMessage extends Message {\n    setup() {\n        super.setup(...arguments);\n        onWillStart(async () => {\n            this.isPortalUser = await user.hasGroup(\"base.group_portal\");\n            this.isInternalUser = await user.hasGroup(\"base.group_user\");\n        });\n    }\n\n    get quickActionCount() {\n        return 3;\n    }\n\n    get canToggleStar() {\n        return super.canToggleStar && this.isInternalUser;\n    }\n    get showUnfollow() {\n        return super.showUnfollow && this.isInternalUser;\n    }\n}\n", "/* @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { messageActionsRegistry } from \"@mail/core/common/message_actions\";\n\nmessageActionsRegistry.add(\"closeThread\", {\n    condition: (component) => component.env.closeThread && !component.env.isResolved(),\n    icon: \"fa fa-check\",\n    title: () => _t(\"Mark the discussion as resolved\"),\n    onClick: (component) => component.env.closeThread(),\n    sequence: 0,\n});\n\nmessageActionsRegistry.add(\"openThread\", {\n    condition: (component) => component.env.openThread && component.env.isResolved(),\n    icon: \"fa fa-retweet\",\n    title: () => _t(\"Re-open the discussion\"),\n    onClick: (component) => component.env.openThread(),\n    sequence: 0,\n});\n", "import { Message } from \"@mail/core/common/message_model\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { url } from \"@web/core/utils/urls\";\n\npatch(Message.prototype, {\n    async copyLink() {\n        if (this.thread?.model === \"knowledge.article.thread\") {\n            let notification = _t(\"Message Link Copied!\");\n            let type = \"info\";\n            try {\n                await browser.navigator.clipboard.writeText(\n                    url(`/knowledge/article/${this.thread.articleId}`)\n                );\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        } else {\n            return super.copyLink(...arguments);\n        }\n    },\n});\n", "import { Message } from \"@mail/core/common/message\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Message.prototype, {\n    /**\n     * This function overrides the original method so that when the user tries to open a the record\n     * from a starred discussion linked to a knowledge thread, it can be redirected to the corresponding\n     * article. This is needed to avoid the user being redirected to the technical view of the\n     * knowledge.article.thread model.\n     * @override\n     */\n    async openRecord() {\n        if (this.message.model === \"knowledge.article.thread\") {\n            const [articleThread] = await this.env.services.orm.searchRead(\n                \"knowledge.article.thread\",\n                [[\"id\", \"=\", this.message.thread.id]],\n                [\"article_id\"]\n            );\n            this.action.doAction({\n                type: \"ir.actions.act_window\",\n                res_id: articleThread.article_id[0],\n                res_model: \"knowledge.article\",\n                views: [[false, \"form\"]],\n            });\n            return;\n        }\n        super.openRecord();\n    },\n});\n", "import { KnowledgeMessage } from \"@knowledge/mail/message/knowledge_message\";\nimport { Thread } from \"@mail/core/common/thread\";\nimport { onMounted } from \"@odoo/owl\";\n\nexport class KnowledgeThread extends Thread {\n    static components = { ...Thread.components, Message: KnowledgeMessage };\n\n    setup() {\n        super.setup();\n        this.props.thread.knowledgePreLoading = true;\n        onMounted(() => {\n            this.props.thread.knowledgePreLoading = false;\n        });\n    }\n}\n", "/* @odoo-module */\n\nimport { threadActionsRegistry } from \"@mail/core/common/thread_actions\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(threadActionsRegistry.get(\"expand-form\"), {\n    async open(component) {\n        if (component.thread.model === \"knowledge.article.thread\") {\n            const [articleThreadData] = await component.env.services.orm.read(\n                \"knowledge.article.thread\",\n                [component.thread.id],\n                [\"article_id\"],\n                { load: false }\n            );\n            component.actionService.doAction(\"knowledge.ir_actions_server_knowledge_home_page\", {\n                stackPosition: \"replaceCurrentAction\",\n                additionalContext: {\n                    res_id: articleThreadData[\"article_id\"],\n                },\n            });\n            await component.props.chatWindow.close();\n        } else {\n            super.open(component);\n        }\n    },\n});\n", "import { Thread } from \"@mail/core/common/thread_model\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Thread.prototype, {\n    /** @type {boolean|undefined} */\n    knowledgePreLoading: undefined,\n    articleId: undefined,\n\n    /**\n     * @override\n     */\n    async fetchMessagesData({ after, around, before }) {\n        if (!this.knowledgePreLoading) {\n            return await super.fetchMessagesData(...arguments);\n        } else {\n            return await this.store.env.services[\"knowledge.comments\"].fetchMessages(this.id);\n        }\n    },\n});\n", "/** @odoo-module **/\n\nimport { SearchModel } from \"@web/search/search_model\";\n\nexport const KnowledgeSearchModelMixin = (T) => class extends T {\n    setup(services, args) {\n        this.saveEmbeddedViewFavoriteFilter = args.saveEmbeddedViewFavoriteFilter;\n        this.deleteEmbeddedViewFavoriteFilter = args.deleteEmbeddedViewFavoriteFilter;\n        super.setup(services, args);\n    }\n\n    /**\n     * Favorites for embedded views\n     * @override\n     */\n    async load(config) {\n        await super.load(config);\n        if (config.state && !this.isStateCompleteForEmbeddedView) {\n            // If the config contains an imported state that is not directly\n            // coming from a view that was embedded in Knowledge, the favorite\n            // filters have to be loaded, since they come from the\n            // `data-embedded-props` attribute of the anchor for the\n            // EmbeddedViewComponent. Otherwise, those are already specified in\n            // the state and they should not be duplicated.\n            let defaultFavoriteId = null;\n            const activateFavorite = \"activateFavorite\" in config ? config.activateFavorite : true;\n            if (activateFavorite) {\n                defaultFavoriteId = this._createGroupOfFavorites(this.irFilters || []);\n                if (defaultFavoriteId) {\n                    // activate default search items (populate this.query)\n                    this._activateDefaultSearchItems(defaultFavoriteId);\n                }\n            }\n        }\n    }\n\n    /**\n     * Save in embedded view arch instead of creating a record\n     * @override\n     */\n    async _createIrFilters(irFilter) {\n        this.saveEmbeddedViewFavoriteFilter(irFilter);\n        return null;\n    }\n\n    /**\n     * Delete from the embedded view arch instead of deleting the record\n     * @override\n     */\n    async _deleteIrFilters(searchItem) {\n        this.deleteEmbeddedViewFavoriteFilter(searchItem);\n    }\n\n    /**\n     * @override\n     * @returns {Object}\n     */\n    exportState() {\n        const state = super.exportState();\n        state.isStateCompleteForEmbeddedView = true;\n        return state;\n    }\n\n    /**\n     * @override\n     */\n    _importState(state) {\n        super._importState(state);\n        this.isStateCompleteForEmbeddedView = state.isStateCompleteForEmbeddedView;\n    }\n};\n\nexport class KnowledgeSearchModel extends KnowledgeSearchModelMixin(SearchModel) {}\n", "/** @odoo-module */\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Deferred, KeepLast } from \"@web/core/utils/concurrency\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { Chatter } from \"@mail/chatter/web_portal/chatter\";\nimport { useCallbackRecorder } from \"@web/search/action_hook\";\nimport {\n    onWillUnmount,\n    useEffect,\n} from \"@odoo/owl\";\n\n/**\n * Knowledge articles can interact with some records with the help of the\n * @see KnowledgeCommandsService .\n * If any record in a form view has a chatter with the ability to send message\n * and/or attach files, they are a potential target for Knowledge macros.\n */\nconst ChatterPatch = {\n    setup() {\n        super.setup(...arguments);\n        if (this.env.__knowledgeUpdateCommandsRecordInfo__) {\n            this.knowledgeCommandsService = useService(\"knowledgeCommandsService\");\n            // Only keep the last request to register a recordInfo active.\n            const keepLastRecordInfoRequest = new KeepLast();\n            // Keep track of the fact that the chatter thread is ready and has\n            // loaded its access rights.\n            let chatterThreadReady = new Deferred();\n            let previousThreadId = this.props.threadId;\n            useEffect(\n                // Access rights of the current thread of the chatter are\n                // populated asynchronously after the chatter is mounted, this\n                // method keeps track of those changes in order to determine\n                // when a recordInfo request can be evaluated.\n                (threadId, canPostOnReadonly, hasReadAccess, hasWriteAccess) => {\n                    if (previousThreadId !== threadId) {\n                        // If the chatter changes threadId, resolve the current\n                        // promise keeping track of the thread state to false,\n                        // so that an ongoing request to evaluate a recordInfo\n                        // (related to the previous thread) will be discarded.\n                        chatterThreadReady.resolve(false);\n                        chatterThreadReady = new Deferred();\n                    }\n                    if (\n                        canPostOnReadonly !== undefined &&\n                        hasReadAccess !== undefined &&\n                        hasWriteAccess !== undefined\n                    ) {\n                        // When the access rights are all loaded, the thread\n                        // is ready and a recordInfo request can be evaluated.\n                        chatterThreadReady.resolve(true);\n                    }\n                    previousThreadId = threadId;\n                },\n                () => [\n                    this.props.threadId,\n                    this.state.thread?.canPostOnReadonly,\n                    this.state.thread?.hasReadAccess,\n                    this.state.thread?.hasWriteAccess,\n                ]\n            );\n            onWillUnmount(() => {\n                // If there is an ongoing request to evaluate a recordInfo,\n                // discard it.\n                chatterThreadReady.resolve(false);\n            });\n            useCallbackRecorder(\n                this.env.__knowledgeUpdateCommandsRecordInfo__,\n                // Callback used to record the values related to the ability to\n                // post messages or attach files on the current record.\n                async (recordInfo) => {\n                    // At each new recording request, all previous ongoing\n                    // requests are discarded through the keepLast.\n                    const chatterThreadReadyForRecordInfo = keepLastRecordInfoRequest.add(chatterThreadReady);\n                    if (!await chatterThreadReadyForRecordInfo) {\n                        // If the chatterThreadReadyForRecordInfo promise\n                        // resolves to false, the recording request should be\n                        // discarded.\n                        return;\n                    }\n                    if (\n                        !this.env.model.root?.resId ||\n                        recordInfo.resId !== this.env.model.root.resId ||\n                        recordInfo.resModel !== this.env.model.root.resModel\n                    ) {\n                        // Ensure that the current record matches the recordInfo\n                        // candidate.\n                        return;\n                    }\n                    // The conditions for the ability to post or attach should\n                    // be the same as the ones in the Chatter template.\n                    Object.assign(recordInfo, {\n                        canPostMessages:\n                            this.props.threadId &&\n                            (this.state.thread?.hasWriteAccess ||\n                                (this.state.thread?.hasReadAccess &&\n                                    this.state.thread?.canPostOnReadonly)),\n                        canAttachFiles: this.props.threadId && this.state.thread?.hasWriteAccess,\n                    });\n                    if (this.knowledgeCommandsService.isRecordCompatibleWithMacro(recordInfo)) {\n                        this.knowledgeCommandsService.setCommandsRecordInfo(recordInfo);\n                    }\n                }\n            );\n        }\n    }\n};\n\npatch(Chatter.prototype, ChatterPatch);\n", "/** @odoo-module **/\n\nimport { AlertDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { FormController } from '@web/views/form/form_controller';\nimport { KnowledgeSidebar } from '@knowledge/components/sidebar/sidebar';\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\n\nimport {\n    onMounted,\n    onWillStart,\n    useChildSubEnv,\n    useEffect,\n    useExternalListener,\n    useRef,\n} from \"@odoo/owl\";\n\nexport class KnowledgeArticleFormController extends FormController {\n    static template = \"knowledge.ArticleFormView\";\n    // Open articles in edit mode by default\n    static defaultProps = {\n        ...FormController.defaultProps,\n        mode: \"edit\",\n    };\n    static components = {\n        ...FormController.components,\n        KnowledgeSidebar,\n    };\n\n    setup() {\n        super.setup();\n        this.root = useRef('root');\n        this.orm = useService('orm');\n        this.actionService = useService('action');\n        this.dialogService = useService(\"dialog\");\n\n        /*\n            Because of the way OWL is designed we are never sure when OWL finishes mounting this component.\n            Thus, we added this deferred promise in order for us to know when it is done.\n            It is necessary to have this because the comments handler needs to notify the topbar when\n            it has detected comments so that it can show the comments panel's button.\n        */\n        this.topbarMountedPromise = new Deferred();\n\n        useChildSubEnv({\n            createArticle: this.createArticle.bind(this),\n            ensureArticleName: this.ensureArticleName.bind(this),\n            openArticle: this.openArticle.bind(this),\n            renameArticle: this.renameArticle.bind(this),\n            toggleAsideMobile: this.toggleAsideMobile.bind(this),\n            topbarMountedPromise: this.topbarMountedPromise,\n            save: this.save.bind(this),\n            discard: this.discard.bind(this),\n        });\n\n        useBus(this.env.bus, 'KNOWLEDGE:OPEN_ARTICLE', (event) => {\n            this.openArticle(event.detail.id);\n        });\n\n        // Unregister the current candidate recordInfo for Knowledge macros in\n        // case of breadcrumbs mismatch.\n        onWillStart(() => {\n            if (\n                !this.env.inDialog &&\n                this.env.config.breadcrumbs &&\n                this.env.config.breadcrumbs.length\n            ) {\n                // Unregister the current candidate recordInfo in case of\n                // breadcrumbs mismatch.\n                this.knowledgeCommandsService.unregisterCommandsRecordInfo(this.env.config.breadcrumbs);\n            }\n        });\n        onMounted(() => {\n            this.topbarMountedPromise.resolve();\n        });\n\n        useExternalListener(document.documentElement, 'mouseleave', async () => {\n            if (await this.model.root.isDirty()) {\n                await this.model.root.save();\n            }\n        });\n\n        useEffect(\n            () => {\n                const scrollView = this.root.el?.querySelector(\".o_scroll_view_lg\");\n                if (scrollView) {\n                    scrollView.scrollTop = 0;\n                }\n                const mobileScrollView = this.root.el?.querySelector(\".o_knowledge_main_view\");\n                if (mobileScrollView) {\n                    mobileScrollView.scrollTop = 0;\n                }\n            },\n            () => [this.model.root.resId]\n        );\n    }\n\n    /**\n     * Ensure that the title is set @see beforeUnload\n     * Dirty check is sometimes necessary in cases where the user leaves\n     * the article from inside an article (i.e. embedded views/links) very\n     * shortly after a mutation (i.e. in tours). At that point, the\n     * html_field may not have notified the model from the change.\n     * @override\n     */\n    async beforeLeave() {\n        if (this.model.root.resId) {\n            await this.ensureArticleName();\n        }\n        await this.model.root.isDirty();\n        return super.beforeLeave();\n    }\n\n    /**\n     * Check that the title is set or not before closing the tab and\n     * save the whole article, if the current article exists (it does\n     * not exist if there are no articles to show, in which case the no\n     * content helper is displayed).\n     * @override\n     */\n    async beforeUnload(ev) {\n        if (this.model.root.resId) {\n            await this.ensureArticleName();\n            if (await this.model.root.isDirty()) {\n                await super.beforeUnload(ev); // triggers an urgent save\n            }\n        }\n    }\n\n    /**\n     * If the article has no name set, tries to rename it.\n     */\n    ensureArticleName() {\n        const recordData = this.model.root.data;\n        if (\n            !recordData.name &&\n            !(recordData.is_locked || !recordData.user_can_write || !recordData.active)\n        ) {\n            return this.renameArticle();\n        }\n    }\n\n    get resId() {\n        return this.model.root.resId;\n    }\n\n    /**\n     * Create a new article and open it.\n     * @param {String} category - Category of the new article\n     * @param {integer} targetParentId - Id of the parent of the new article (optional)\n     */\n    async createArticle(category, targetParentId) {\n        const articleId = await this.orm.call(\n            \"knowledge.article\",\n            \"article_create\",\n            [],\n            {\n                is_private: category === 'private',\n                parent_id: targetParentId ? targetParentId : false\n            }\n        );\n        this.openArticle(articleId);\n    }\n\n    getHtmlTitle() {\n        const titleEl = this.root.el.querySelector(\".note-editable.odoo-editor-editable h1\");\n        if (titleEl) {\n            const title = titleEl.textContent.trim();\n            if (title) {\n                return title;\n            }\n        }\n    }\n\n    displayName() {\n        return this.model.root.data.name || _t(\"New\");\n    }\n\n    /**\n     * Callback executed before the record save (if the record is valid).\n     * When an article has no name set, use the title (first h1 in the\n     * body) to try to save the article with a name.\n     * @overwrite\n     */\n    async onWillSaveRecord(record, changes) {\n        if (!record.data.name) {\n            const title = this.getHtmlTitle();\n            if (title) {\n                changes.name = title;\n            }\n         }\n    }\n\n    /**\n     * @param {integer} - resId: id of the article to open\n     */\n    async openArticle(resId) {\n        if (!resId || resId === this.resId) {\n            return;\n        }\n\n        // blur to remove focus on the active element\n        document.activeElement.blur();\n\n        // load the new record\n        try {\n            if (this.model.root.isNew) {\n                await this.model.load({ resId });\n            } else {\n                await this.ensureArticleName();\n                if (await this.model.root.isDirty()) {\n                    await this.model.root.save({\n                        onError: this.onSaveError.bind(this),\n                        nextId: resId,\n                    });\n                } else {\n                    await this.model.load({ resId });\n                }\n            }\n        } catch {\n            this.dialogService.add(AlertDialog, {\n                title: _t(\"Access Denied\"),\n                body: _t(\n                    \"The article you are trying to open has either been removed or is inaccessible.\",\n                ),\n                confirmLabel: _t(\"Close\"),\n            });\n        }\n        this.toggleAsideMobile(false);\n    }\n\n    /*\n     * Rename the article using the given name, or using the article title if\n     * no name is given (first h1 in the body). If no title is found, the\n     * article is kept untitled.\n     * @param {string} name - new name of the article\n     */\n    renameArticle(name) {\n        if (!name) {\n            const title = this.getHtmlTitle();\n            if (!title) {\n                return;\n            }\n            name = title;\n        }\n        return this.model.root.update({ name });\n    }\n\n    /**\n     * Toggle the aside menu on mobile devices (< 576px).\n     * @param {boolean} force\n     */\n    toggleAsideMobile(force) {\n        const container = this.root.el.querySelector('.o_knowledge_form_view');\n        container.classList.toggle('o_toggle_aside', force);\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 { FormRenderer } from '@web/views/form/form_renderer';\nimport { KnowledgeCoverDialog } from '@knowledge/components/knowledge_cover/knowledge_cover_dialog';\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useChildSubEnv, useExternalListener, useRef } from \"@odoo/owl\";\n\nexport class KnowledgeArticleFormRenderer extends FormRenderer {\n\n    //--------------------------------------------------------------------------\n    // Component\n    //--------------------------------------------------------------------------\n    setup() {\n        super.setup();\n\n        this.actionService = useService(\"action\");\n        this.dialog = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n\n        this.root = useRef('compiled_view_root');\n\n        useChildSubEnv({\n            openCoverSelector: this.openCoverSelector.bind(this),\n            config: this.env.config,\n            toggleFavorite: this.toggleFavorite.bind(this),\n            _saveIfDirty: this._saveIfDirty.bind(this),\n        });\n\n        useExternalListener(document, \"click\", event => {\n            if (event.target.classList.contains(\"o_nocontent_create_btn\")) {\n                this.env.createArticle(\"private\");\n            }\n        });\n    }\n\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    openCoverSelector() {\n        this.dialog.add(KnowledgeCoverDialog, {\n            articleCoverId: this.props.record.data.cover_image_id[0],\n            articleName: this.props.record.data.name || \"\",\n            save: (id) => this.props.record.update({cover_image_id: [id]})\n        });\n    }\n\n    get resId() {\n        return this.props.record.resId;\n    }\n\n    /**\n     * Add/Remove article from favorites and reload the favorite tree.\n     * One does not use \"record.update\" since the article could be in readonly.\n     * @param {event} Event\n     */\n    async toggleFavorite(event) {\n        // Save in case name has been edited, so that this new name is used\n        // when adding the article in the favorite section.\n        await this._saveIfDirty();\n        await this.orm.call(this.props.record.resModel, \"action_toggle_favorite\", [[this.resId]]);\n        // Load to have the correct value for 'is_user_favorite'.\n        await this.props.record.load();\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    async _saveIfDirty() {\n        if (await this.props.record.isDirty()) {\n            await this.props.record.save();\n        }\n    }\n\n    _scrollToElement(container, element) {\n        const rect = element.getBoundingClientRect();\n        container.scrollTo(rect.left, rect.top);\n    }\n}\n", "/** @odoo-module */\n\nimport { formView } from '@web/views/form/form_view';\nimport { registry } from \"@web/core/registry\";\nimport { KnowledgeArticleFormController } from './knowledge_controller.js';\nimport { KnowledgeArticleFormRenderer } from './knowledge_renderers.js';\n\nexport const knowledgeArticleFormView = {\n    ...formView,\n    Controller: KnowledgeArticleFormController,\n    Renderer: KnowledgeArticleFormRenderer,\n    display: {controlPanel: false}\n};\n\nregistry.category('views').add('knowledge_article_view_form', knowledgeArticleFormView);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\n\nconst commandCategoryRegistry = registry.category(\"command_categories\");\n// displays the articles on input \"?\"\ncommandCategoryRegistry.add(\"knowledge_articles\", { namespace: \"?\" }, { sequence: 10 });\n// displays the advanced search command on input \"?\"\ncommandCategoryRegistry.add(\"knowledge_extra\", { namespace: \"?\" }, { sequence: 20 });\n", "/** @odoo-module */\n\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { HotkeyCommandItem } from \"@web/core/commands/default_providers\";\nimport { DefaultCommandItem, splitCommandName } from \"@web/core/commands/command_palette\";\nimport { markup } from \"@odoo/owl\";\nimport { user } from \"@web/core/user\";\n\n// Articles command\nclass KnowledgeCommand extends DefaultCommandItem {\n    static template = \"KnowledgeCommandTemplate\";\n    static props = {\n        ...DefaultCommandItem.props,\n        headline: String,\n        icon_string: String,\n        isFavorite: Boolean,\n        splitSubjectName: Array,\n        subjectName: [String, Boolean],\n    };\n}\n\n// \"Not found, create one\" command\nclass Knowledge404Command extends DefaultCommandItem {\n    static template = \"Knowledge404CommandTemplate\";\n    static props = {\n        ...DefaultCommandItem.props,\n        articleName: String,\n    };\n}\n\n// Advanced search command\nclass KnowledgeExtraCommand extends HotkeyCommandItem {\n    static template = \"KnowledgeExtraCommandTemplate\";\n}\n\nconst commandSetupRegistry = registry.category(\"command_setup\");\ncommandSetupRegistry.add(\"?\", {\n    debounceDelay: 500,\n    emptyMessage: _t(\"No article found.\"),\n    name: _t(\"articles\"),\n    placeholder: _t(\"Search for an article...\"),\n});\n\nconst commandProviderRegistry = registry.category(\"command_provider\");\n\nconst fn = (hidden) => {\n    // Check if the user has enough rights to create a new article\n    const canCreate = () => user.checkAccessRight(\"knowledge.article\", \"create\");\n    let articlesData;\n    return async function provide(env, options) {\n        articlesData = await env.services.orm.call(\n            \"knowledge.article\",\n            \"get_user_sorted_articles\",\n            [[]],\n            {\n                search_query: options.searchValue,\n                hidden_mode: hidden,\n            }\n        );\n        if (!hidden){\n            if (articlesData.length === 0) {\n                // Only display the \"create article\" command when the user can\n                // create an article and when the user inputs at least 3 characters\n                if (options.searchValue.length > 2 && await canCreate()) {\n                    return [{\n                        Component: Knowledge404Command,\n                        async action() {\n                            const articleId = await env.services.orm.call(\n                                'knowledge.article',\n                                'article_create',\n                                [options.searchValue],\n                                {\n                                    is_private: true\n                                },\n                            );\n\n                            env.services.action.doAction('knowledge.ir_actions_server_knowledge_home_page', {\n                                additionalContext: {\n                                    res_id: articleId,\n                                }\n                            });\n                        },\n                        name: _t('No Article found. Create \"%s\"', options.searchValue),\n                        props: {\n                            articleName: options.searchValue,\n                        },\n                    }];\n                }\n                else {\n                    return [];\n                }\n            }\n        }\n        const knowledgeMainMenuId = env.services.menu.getAll().find(\n            menu => menu.xmlid === 'knowledge.knowledge_menu_root'\n        ).id;\n        // display the articles\n        const result = articlesData.map(article => ({\n            Component: KnowledgeCommand,\n            action() {\n                env.services.action.doAction('knowledge.ir_actions_server_knowledge_home_page', {\n                    additionalContext: {\n                        res_id: article.id,\n                    }\n                });\n\n            },\n            category: \"knowledge_articles\",\n            href: `/odoo/knowledge.article/${article.id}?menu_id=${knowledgeMainMenuId}`,\n            name: article.name || _t(\"Untitled\"),\n            props: {\n                isFavorite: article.is_user_favorite,\n                headline: article.headline ? markup(article.headline) : '',\n                subjectName: article.root_article_id[0] != article.id ? article.root_article_id[1] : false,\n                splitSubjectName: splitCommandName(article.root_article_id[1], options.searchValue),\n                icon_string: article.icon || '\ud83d\udcc4',\n            },\n        }));\n        if(!hidden){\n        // add the \"advanced search\" command\n            result.push({\n                Component: KnowledgeExtraCommand,\n                async action() {\n                    const articleIds = articlesData.map(article => article.id);\n                    const action = await env.services.action.loadAction('knowledge.knowledge_article_action');\n                    delete action.context.search_default_filter_not_is_article_item;\n                    env.services.action.doAction(action, {\n                        additionalContext: {\n                            search_default_filter_search_article_ids: 1,\n                            search_article_ids: articleIds,\n                        },\n                    });\n                },\n                category: \"knowledge_extra\",\n                name: _t(\"Advanced Search\"),\n                props: {\n                    hotkey: \"alt+B\",\n                },\n            });\n        }\n        return result;\n    };\n};\n\ncommandProviderRegistry.add(\"knowledge\", {\n    debounceDelay: 500,\n    namespace: \"?\",\n    provide: fn(false),\n});\n\ncommandSetupRegistry.add(\"$\", {\n    debounceDelay: 500,\n    emptyMessage: _t(\"Oops, there's nothing here. Try another search.\"),\n    placeholder: _t(\"Search hidden Articles...\"),\n});\ncommandProviderRegistry.add(\"knowledge_members_only_articles\", {\n    debounceDelay: 500,\n    namespace: \"$\",\n    provide: fn(true),\n});\n", "/** @odoo-module */\n\nimport { useService } from '@web/core/utils/hooks';\n\nexport const EmbeddedControllersPatch = (T) => class EmbeddedControllersPatch extends T {\n    /**\n     * @override\n     */\n    setup() {\n        super.setup(...arguments);\n        this.orm = useService('orm');\n    }\n    /**\n     * Action when clicking on the Create button for list and kanban embedded\n     * views (for knowledge.article).\n     * Create an article item and redirect to its form view\n     * Note: If Quick Create is enabled on the view, should use quick create\n     * instead of custom create.\n     *\n     * @override\n     */\n    async createRecord() {\n        const { onCreate } = this.props.archInfo;\n        if (this.canQuickCreate && onCreate === \"quick_create\") {\n            return super.createRecord(...arguments);\n        }\n\n        const articleId = await this.orm.call('knowledge.article', 'article_create', [], {\n            is_article_item: true,\n            is_private: false,\n            parent_id: this.props.context.active_id || false\n        });\n\n        this.props.selectRecord(articleId, {});\n    }\n};\n", "/** @odoo-module **/\n\nimport { EmbeddedControllersPatch } from \"@knowledge/views/embedded_controllers_patch\";\nimport { KanbanController } from \"@web/views/kanban/kanban_controller\";\nimport { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { registry } from \"@web/core/registry\";\n\nexport class KnowledgeArticleItemsKanbanController extends EmbeddedControllersPatch(\n    KanbanController\n) {\n    /**\n     * @override\n     * Some actions require write access on the parent article. Disable those actions if the user\n     * does not have it.\n     */\n    setup() {\n        super.setup();\n        if (!(\"isEmbeddedReadonly\" in this.env) || this.env.isEmbeddedReadonly) {\n            [\"create\", \"createGroup\", \"deleteGroup\", \"editGroup\"].forEach(\n                (action) => (this.props.archInfo.activeActions[action] = false)\n            );\n            this.props.archInfo.groupsDraggable = false;\n            this.props.archInfo.activeActions.quickCreate = false;\n        }\n    }\n}\n\nregistry.category(\"views\").add(\"knowledge_article_view_kanban_embedded\", {\n    ...kanbanView,\n    Controller: KnowledgeArticleItemsKanbanController,\n});\n", "/** @odoo-module */\n\nimport { EmbeddedControllersPatch } from \"@knowledge/views/embedded_controllers_patch\";\nimport { ListController } from \"@web/views/list/list_controller\";\nimport { listView } from \"@web/views/list/list_view\";\nimport { registry } from \"@web/core/registry\";\n\nexport class KnowledgeArticleItemsListController extends EmbeddedControllersPatch(ListController) {\n    /**\n     * @override\n     * Item creation is not allowed if the user can not edit the parent article.\n     */\n    setup() {\n        super.setup();\n        if (!(\"isEmbeddedReadonly\" in this.env) || this.env.isEmbeddedReadonly) {\n            this.activeActions.create = false;\n        }\n    }\n}\n\nregistry.category(\"views\").add(\"knowledge_article_view_tree_embedded\", {\n    ...listView,\n    Controller: KnowledgeArticleItemsListController,\n});\n", "/** @odoo-module */\n\nimport { KnowledgeSearchModelMixin } from \"@knowledge/search_model/search_model\";\nimport { SearchModel } from \"@web/search/search_model\";\nimport { View } from \"@web/views/view\";\n\nexport class EmbeddedView extends View {\n    static props = {\n        ...View.props,\n        saveEmbeddedViewFavoriteFilter: Function,\n        deleteEmbeddedViewFavoriteFilter: Function,\n    };\n\n    async loadView(props) {\n        const {\n            additionalViewProps,\n            saveEmbeddedViewFavoriteFilter,\n            deleteEmbeddedViewFavoriteFilter,\n            ...viewProps\n        } = props;\n        delete viewProps.displayName;\n        Object.assign(viewProps, additionalViewProps);\n        await super.loadView(viewProps);\n        this.withSearchProps.SearchModel = KnowledgeSearchModelMixin(\n            this.withSearchProps.SearchModel || SearchModel\n        );\n        this.withSearchProps.searchModelArgs = {\n            saveEmbeddedViewFavoriteFilter,\n            deleteEmbeddedViewFavoriteFilter,\n        };\n    }\n\n    /**\n     * The super method makes the assumption that the props can only vary in\n     * the search keys, but we cannot make this assumption with embedded views:\n     * the user can edit the additionalViewProps from the embeddedViewComponent\n     */\n    async onWillUpdateProps(nextProps) {\n        super.onWillUpdateProps(nextProps);\n        this.env.config.setDisplayName(nextProps.displayName);\n        if (\n            JSON.stringify(this.props.additionalViewProps) !==\n            JSON.stringify(nextProps.additionalViewProps)\n        ) {\n            Object.assign(this.componentProps, nextProps.additionalViewProps);\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { CalendarModel } from \"@web/views/calendar/calendar_model\";\nimport {\n    serializeDate,\n    serializeDateTime,\n    deserializeDate,\n    deserializeDateTime,\n} from \"@web/core/l10n/dates\";\n\n/**\n * CalendarModel allowing the usage of properties as calendar options.\n * Note: currently, as it is only used in knowledge, the properties\n * field name has been hardcoded for clarity, but if needed it can be\n * easily retrieved from the archInfo.\n */\nexport class ItemCalendarModel extends CalendarModel {\n\n    /**\n     * @override\n     * Prevent creation of record if the user does not have write access on the\n     * parent record (readonly knowledge articles for example) and if the model\n     * props are invalid\n     */\n    get canCreate() {\n        if (this.meta.invalid) {\n            return false;\n        }\n        return this.meta.canCreate;\n    }\n    /**\n     * @override\n     * Usually, records cannot be edited if the start field is readonly (which\n     * is a property shared by every record of the current calendar). In this\n     * case however, it depends on the record (eg. users cannot edit readonly\n     * knowledge articles). Therefore, we always allow users to edit the record\n     * but we show an access error if the user tried to edit a readonly record.\n     */\n    get canEdit() {\n        return true;\n    }\n\n    /**\n     * @override\n     * Show the \"all day\" slot in \"day\" and \"week\" scales even if the date is a\n     * datetime, as there is no \"allDay\" option in this model.\n     */\n    get hasAllDaySlot() {\n        return true;\n    }\n\n    /**\n     * @override\n     * Build a raw record from the given partial record and the properties. We\n     * need every property of the record when editing a record otherwise we \n     * will lose the values of these properties when writing on the record\n     * (since the properties are stored as json objects).\n     */\n    buildRawRecord(partialRecord, options = {}) {\n        let start = partialRecord.start;\n        let end = partialRecord.end;\n\n        if (!end || !end.isValid) {\n            // Set end date if not existing\n            if (this.meta.propertiesDateType === \"date\") {\n                end = start;\n            } else {\n                end = start.plus({ hours: 1 });\n            }\n        }\n\n        if (this.meta.propertiesDateType === \"datetime\" && partialRecord.isAllDay) {\n            if (partialRecord.id) {\n                // Keep time of the record when moving or resizing a datetime\n                // record in an allDay scale (eg. in month scale, there is no\n                // time scale so the partialRecord is created as an allDay\n                // record even if the record moved it is a datetime)\n                start = start.set({ hours: this.data.records[partialRecord.id].start.hour});\n                end = end.set({ hours: this.data.records[partialRecord.id].end.hour });  \n            } else {\n                // When creating a datetime that spans multiple days, set\n                // arbitrary start and end hours (to not have midnight by\n                // default)\n                start = start.set({hours: 7});\n                end = end.set({hours: 19});\n            }\n        }\n\n        start = this.meta.propertiesDateType === \"date\" ? serializeDate(start) : serializeDateTime(start);\n        end = this.meta.propertiesDateType === \"date\" ? serializeDate(end) : serializeDateTime(end);\n\n        let properties = {};\n\n        if (partialRecord.id) {\n            // Copy the properties of the existing rawRecord but update the\n            // start and stop date properties\n            properties = this.data.records[partialRecord.id].rawRecord.article_properties;\n            properties.find(property => property.name === this.meta.fieldMapping.date_start).value = start;\n\n            if (this.meta.fieldMapping.date_stop) {\n                properties.find(property => property.name === this.meta.fieldMapping.date_stop).value = end;\n            }\n        } else {\n            // Create the start and stop date properties for the new record\n            properties[this.meta.fieldMapping.date_start] = start;\n            if (this.meta.fieldMapping.date_stop) {\n                properties[this.meta.fieldMapping.date_stop] = end;\n            }\n        }\n        return {article_properties: properties};\n    }\n\n    /**\n     * If the model is invalid (missing props needed to fetch the correct\n     * items after a reload for example), no record should be loaded.\n     */\n    loadRecords(data) {\n        if (this.meta.invalid) {\n            return {};\n        }\n        return super.loadRecords(data);\n    }\n\n    /**\n     * @override\n     * Normalize the given raw record with the properties\n     */\n    normalizeRecord(rawRecord) {\n        const { fieldMapping, propertiesDateType, scale } = this.meta;\n        const isDate = propertiesDateType === \"date\";\n\n        const startValue = rawRecord.article_properties.find(property => property.name === fieldMapping.date_start)?.value;\n        const stopValue = rawRecord.article_properties.find(property => property.name === fieldMapping.date_stop)?.value;\n        const start = isDate\n            ? deserializeDate(startValue)\n            : deserializeDateTime(startValue);\n\n        // If the stop property is not set, use the same date as the start\n        // to not show an invalid date message\n        const end = stopValue ?\n            isDate\n                ? deserializeDate(stopValue)\n                : deserializeDateTime(stopValue)\n            : start;\n\n        // The time of the record is shown in the calendar if it is a datetime\n        // and if the selected scale is month (because there is no time scale\n        // in this case)\n        const showTime = scale === \"month\" && !isDate;\n            \n        const colorValue = rawRecord.article_properties.find(property => property.name === fieldMapping.color)?.value;\n        const colorIndex = Array.isArray(colorValue) ? colorValue[0] : colorValue;\n\n        return {\n            id: rawRecord.id,\n            title: rawRecord.display_name,\n            isAllDay: isDate,\n            start,\n            startType: propertiesDateType,\n            end,\n            endType: propertiesDateType,\n            colorIndex,\n            isTimeHidden: !showTime,\n            rawRecord,\n        };\n    }\n\n    /**\n     * @override\n     * Compute the domain used to fetch the items using the properties used as\n     * start and end date. The properties definition is used in the domain to\n     * make sure to not match records that still use the \"previous\" dateStart\n     * property when this property has been changed (if the type of a property\n     * changed, the property will be replaced by a new one but the change will\n     * be propagated to the records using that property when writing on them)\n     */\n    computeRangeDomain(data) {\n        const { fieldMapping } = this.meta;\n        const formattedEnd = serializeDateTime(data.range.end);\n        const formattedStart = serializeDateTime(data.range.start);\n\n        const domain = [\n            [`article_properties.${fieldMapping.date_start}`, \"<=\", formattedEnd],\n            ['parent_id.article_properties_definition', 'ilike', `\"${fieldMapping.date_start}\"`]\n        ];\n        if (fieldMapping.date_stop) {\n            domain.push(\n                [`article_properties.${fieldMapping.date_stop}`, \">=\", formattedStart],\n                ['parent_id.article_properties_definition', 'ilike', `\"${fieldMapping.date_stop}\"`]\n            );\n        }\n        return domain;\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { CalendarCommonPopover } from \"@web/views/calendar/calendar_common/calendar_common_popover\";\nimport { CalendarCommonRenderer } from \"@web/views/calendar/calendar_common/calendar_common_renderer\";\nimport { CalendarController } from \"@web/views/calendar/calendar_controller\";\nimport { CalendarRenderer } from \"@web/views/calendar/calendar_renderer\";\nimport { calendarView } from \"@web/views/calendar/calendar_view\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { ItemCalendarModel } from \"@knowledge/views/item_calendar/item_calendar_model\";\nimport { registry } from \"@web/core/registry\";\nimport { onMounted, onWillUpdateProps, useEffect } from \"@odoo/owl\";\n\nexport class KnowledgeArticleItemsCalendarController extends CalendarController {\n    static template = \"knowledge.ArticleItemsCalendarController\";\n\n    setup() {\n        super.setup();\n        // Item creation is not allowed if the user can not edit the parent article.\n        if (!(\"isEmbeddedReadonly\" in this.env) || this.env.isEmbeddedReadonly) {\n            this.model.meta.canCreate = false;\n        }\n        onMounted(async () => {\n            // Show error message if the start date property is invalid (if it\n            // has been deleted or its type changed)\n            if (!this.model.meta.invalid && Object.keys(this.model.data.records).length === 0) {\n                const propertiesDefinition = await this.orm.read(\n                    this.props.resModel,\n                    [this.props.context.active_id],\n                    [\"article_properties_definition\"]\n                );\n                this.state.missingConfiguration =\n                    !propertiesDefinition[0].article_properties_definition.some(\n                        (property) =>\n                            property.name === this.props.itemCalendarProps.dateStartPropertyId\n                    );\n            }\n        });\n        onWillUpdateProps((nextProps) => {\n            // Update the model if the itemCalendarProps were updated\n            if (\n                JSON.stringify(this.props.itemCalendarProps) !==\n                JSON.stringify(nextProps.itemCalendarProps)\n            ) {\n                this.updateModel(nextProps.itemCalendarProps);\n                this.state.isWeekendVisible =\n                    nextProps.itemCalendarProps.showWeekEnds ?? this.state.isWeekendVisible;\n                this.state.missingConfiguration = false;\n            }\n        });\n    }\n\n    get showSideBar() {\n        // Hide sideabar when the view is embedded\n        return !this.env.isEmbeddedView && super.showSideBar;\n    }\n\n    /**\n     * Create a new article item and open it. If no record is given, creates it\n     * without any start/stop date properties.\n     * @param {Object} record: calendar record used to create the item with\n     * properties.\n     */\n    async createRecord(record) {\n        if (this.model.canCreate) {\n            const createValues = {\n                is_article_item: true,\n                parent_id: this.props.context.active_id || false,\n            };\n            if (record) {\n                const rawRecord = this.model.buildRawRecord(record);\n                Object.assign(createValues, rawRecord);\n            }\n            const articleId = await this.orm.call(\n                \"knowledge.article\",\n                \"article_create\",\n                [],\n                createValues\n            );\n            this.selectRecord(articleId);\n        }\n    }\n\n    /**\n     * Send the item to the trash\n     */\n    deleteRecord(record) {\n        this.displayDialog(ConfirmationDialog, {\n            title: _t(\"Confirmation\"),\n            body: _t(\"Are you sure you want to send this article to the trash?\"),\n            confirm: async () => {\n                await this.orm.call(\"knowledge.article\", \"action_send_to_trash\", [record.id]);\n                this.model.load();\n            },\n            confirmLabel: _t(\"Send to trash\"),\n            cancel: () => {\n                // `ConfirmationDialog` needs this prop to display the cancel\n                // button but we do nothing on cancel.\n            },\n        });\n    }\n\n    /**\n     * @override\n     * Open an item to allow editing it. This override allows to load the article from the\n     * KnowledgeController if the view is embedded, allowing to preserve the navigation history\n     */\n    editRecord(record) {\n        this.selectRecord(record.id);\n    }\n\n    /**\n     * Set model meta variables to make the model work with the properties\n     */\n    onWillStartModel() {\n        if (this.props.itemCalendarProps) {\n            this.updateModel(this.props.itemCalendarProps);\n            this.state.isWeekendVisible =\n                this.props.itemCalendarProps.showWeekEnds ?? this.state.isWeekendVisible;\n        } else {\n            this.state.missingConfiguration = true;\n            this.model.meta.invalid = true;\n        }\n    }\n\n    /**\n     * Open the given article.\n     * If the view is embedded, the article is loaded from the KnowledgeController to preserve\n     * navigation history.\n     * If the view is opened in full screen, the home page action with the given article is loaded\n     * because the knowledge_article_action_item_calendar action does not have a form view (hence,\n     * props.selectRecord does not work).\n     */\n    selectRecord(articleId) {\n        if (this.env.isEmbeddedView) {\n            this.props.selectRecord(articleId);\n        } else {\n            this.action.doAction(\n                this.orm.call(\"knowledge.article\", \"action_home_page\", [articleId]),\n                {}\n            );\n        }\n    }\n\n    /**\n     * Update the model field mappings and other meta variables using the given\n     * modelProps.\n     */\n    updateModel(modelProps) {\n        this.model.meta.fieldMapping.date_start = modelProps.dateStartPropertyId;\n        this.model.meta.fieldMapping.date_stop = modelProps.dateStopPropertyId;\n        this.model.meta.fieldMapping.color = modelProps.colorPropertyId;\n        this.model.meta.propertiesDateType = modelProps.dateType;\n        if (modelProps.scale) {\n            this.model.meta.scale = modelProps.scale;\n        }\n    }\n\n    get rendererProps() {\n        return {\n            ...super.rendererProps,\n            slotMaxTime: this.props.itemCalendarProps?.slotMaxTime || \"24:00\",\n            slotMinTime: this.props.itemCalendarProps?.slotMinTime || \"00:00\",\n        };\n    }\n}\n\nclass KnowledgeArticleItemsCommonPopover extends CalendarCommonPopover {\n    static subTemplates = {\n        ...CalendarCommonPopover.subTemplates,\n        body: \"knowledge.ArticleItemsCalendarCommonPopover.body\",\n        footer: \"knowledge.ArticleItemsCalendarCommonPopover.footer\",\n    };\n\n    /**\n     * Delete permission should not be based on the view's delete parameter only, but on the\n     * user's write permission on the item as well.\n     */\n    get isEventDeletable() {\n        return super.isEventDeletable && this.props.record.rawRecord.user_can_write;\n    }\n}\n\nclass KnowledgeArticleItemsCommonRenderer extends CalendarCommonRenderer {\n    static props = {\n        ...CalendarCommonRenderer.props,\n        slotMinTime: { type: String },\n        slotMaxTime: { type: String },\n    };\n    static components = {\n        ...CalendarCommonRenderer.components,\n        Popover: KnowledgeArticleItemsCommonPopover,\n    };\n\n    get options() {\n        return {\n            ...super.options,\n            slotMinTime: this.props.slotMinTime,\n            slotMaxTime: this.props.slotMaxTime,\n        };\n    }\n\n    setup() {\n        super.setup();\n        useEffect(\n            (slotMinTime, slotMaxTime) => {\n                this.fc.api.setOption(\"slotMinTime\", slotMinTime);\n                this.fc.api.setOption(\"slotMaxTime\", slotMaxTime);\n            },\n            () => [this.options.slotMinTime, this.options.slotMaxTime]\n        );\n    }\n}\n\nclass KnowledgeArticleItemsCalendarRenderer extends CalendarRenderer {\n    static props = {\n        ...CalendarRenderer.props,\n        slotMinTime: { type: String },\n        slotMaxTime: { type: String },\n    };\n    static components = {\n        ...CalendarRenderer.components,\n        day: KnowledgeArticleItemsCommonRenderer,\n        week: KnowledgeArticleItemsCommonRenderer,\n        month: KnowledgeArticleItemsCommonRenderer,\n    };\n}\n\nregistry.category(\"views\").add(\"knowledge_article_view_calendar_embedded\", {\n    ...calendarView,\n    Controller: KnowledgeArticleItemsCalendarController,\n    Model: ItemCalendarModel,\n    Renderer: KnowledgeArticleItemsCalendarRenderer,\n});\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from '@web/core/registry';\nimport { user } from \"@web/core/user\";\nimport { listView } from '@web/views/list/list_view';\nimport { ListController } from '@web/views/list/list_controller';\n\n\nexport class KnowledgeArticleController extends ListController {\n    setup() {\n        super.setup();\n        // Hide create button (creation cannot be deactivated to allow imports)\n        this.activeActions.create = false;\n    }\n\n    /**\n     * Add the \"duplicate\" action in the additional actions of the list view.\n     * Place it as the second additional action for admin users.\n     * @override\n     */\n    getStaticActionMenuItems() {\n        const menuItems = super.getStaticActionMenuItems();\n        menuItems.duplicate = {\n            isAvailable: () => this.nbSelected && user.isAdmin,\n            sequence: 15,\n            icon: \"fa fa-clone\",\n            description: _t(\"Duplicate\"),\n            callback: async () => {\n                const selectedResIds = await this.getSelectedResIds();\n                if (selectedResIds.length === 1) {\n                    await this.model.orm.call(this.props.resModel, \"copy\", [selectedResIds]);\n                } else {\n                    await this.model.orm.call(this.props.resModel, \"copy_batch\", [selectedResIds]);\n                }\n                this.model.load();\n            },\n        };\n        return menuItems;\n    }\n}\n\nregistry.category(\"views\").add('knowledge_article_view_tree', {\n    ...listView,\n    Controller: KnowledgeArticleController,\n});\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\n\n/**\n * This service store data from non-knowledge form view records that can be used\n * by a Knowledge form view.\n *\n * A typical usage could be the following:\n * - A form view is loaded and one field of the current record is a match for\n *   Knowledge @see FormControllerPatch\n *   - Information about this record and how to access its form view is stored\n *     in this @see KnowledgeCommandsService .\n * - A knowledge Article is opened and it contains a @see TemplateEmbedded .\n *   - When the embedded is injected (@see HtmlFieldPatch ) in the view, it\n *     asks this @see KnowledgeCommandsService if the record can be interacted\n *     with.\n *   - if there is one such record, the related buttons are displayed in the\n *     toolbar of the embedded.\n * - When one such button is used, the form view of the record is reloaded\n *   and the button action is executed through a @see Macro .\n *   - an example of macro action would be copying the template contents as the\n *     value of a field_html of the record, such as \"description\"\n *\n * Scope of the service:\n *\n * Knowledge macros:\n *\n * 1) @see FormControllerPatch :\n *        It will only be called if the viewed record can be used within the\n *        Knowledge module. Such a record should at least have one html field\n *        that is visible and editable by the current user.\n * 2) @see ChatterPatch :\n *        It will be called if the currently viewed record (in a Form view) has\n *        a chatter which the user can use to attach files or send messages.\n * 3) @see TemplateEmbedded or @see FileEmbedded :\n *        It will be called by a embedded to check whether it has a record that\n *        can be interacted with in the context of the toolbar, detected through\n *        case 1) and/or 2): canPostMessages, canAttachFiles, withHtmlField.\n *\n * Knowledge external embedded views insertion:\n *\n * 1) @see EmbeddedViewRendererPatch :\n *        Store information related to an Odoo view in the service, in order\n *        to insert it in a Knowledge article body.\n * 2) @see HtmlFieldPatch :\n *        Recover the previously stored information to perform the insertion.\n */\nexport const knowledgeCommandsService = {\n    start(env) {\n        //----------------------------------------------------------------------\n        // Knowledge macros features\n        //----------------------------------------------------------------------\n\n        // Potential candidate for Knowledge macros.\n        let commandsRecordInfo = null;\n\n        /**\n         * Register data related to a potential record candidate for Knowledge\n         * macros.\n         *\n         * @param {Object|null} recordInfo if null is given, the\n         *        commandsRecordInfo values are reset.\n         * @param {number} [recordInfo.resId] id of the target record\n         * @param {string} [recordInfo.resModel] model name of the target record\n         * @param {Array} [recordInfo.breadcrumbs] array of breadcrumbs objects\n         *                {jsId, name} leading to the target record\n         * @param {boolean} [recordInfo.canPostMessages] target record\n         *                  has a chatter and user can post messages\n         * @param {boolean} [recordInfo.canAttachFiles] target record\n         *                  has a chatter and user can attach files\n         * @param {boolean} [recordInfo.withHtmlField] target record has a\n         *                  targeted html field @see FormControllerPatch\n         * @param {Object} [recordInfo.fieldInfo] info object for the html field\n         *                 {string, name, pageName}\n         * @param {XMLDocument} [recordInfo.xmlDoc] xml document (arch of the\n         *                      form view of the target record)\n         */\n        function setCommandsRecordInfo(recordInfo) {\n            commandsRecordInfo = recordInfo;\n        }\n\n        /**\n         * Get the current record candidate that would be used in Knowledge.\n         */\n        function getCommandsRecordInfo() {\n            return commandsRecordInfo;\n        }\n\n        /**\n         * Copy some actionService breadcrumbs properties used as an identifier\n         * for a recordInfo object.\n         *\n         * @param {Breadcrumbs} breadcrumbs from the @see actionService\n         * @returns {Array[Object]} breadcrumbs identifier containing the jsId\n         *                          and name.\n         */\n        function getBreadcrumbsIdentifier(breadcrumbs) {\n            return breadcrumbs.map(breadcrumb => {\n                return {\n                    jsId: breadcrumb.jsId,\n                    name: breadcrumb.name,\n                };\n            });\n        }\n\n        /**\n         * Evaluate if a record candidate is usable for at least one macro in\n         * Knowledge.\n         *\n         * @param {Object} recordInfo refer to @see setCommandsRecordInfo\n         */\n        function isRecordCompatibleWithMacro(recordInfo) {\n            return recordInfo.canAttachFiles ||\n                recordInfo.canPostMessages ||\n                recordInfo.withHtmlField;\n        }\n\n        /**\n         * Compare the provided breadcrumbs identifier with a previously\n         * registered recordInfo's and unregister the recordInfo if the\n         * registered breadcrumbs are not included at the start of the provided\n         * breadcrumbs.\n         *\n         * @param {Breadcrumbs} breadcrumbs from the @see actionService\n         */\n        function unregisterCommandsRecordInfo(breadcrumbs) {\n            const commandsRecordInfo = getCommandsRecordInfo();\n            if (!commandsRecordInfo) {\n                return;\n            }\n            // Extract identifier props from the breadcrumbs.\n            breadcrumbs = getBreadcrumbsIdentifier(breadcrumbs);\n            if (\n                breadcrumbs.length <= commandsRecordInfo.breadcrumbs.length ||\n                breadcrumbs[commandsRecordInfo.breadcrumbs.length - 1].jsId !== commandsRecordInfo.breadcrumbs.at(-1).jsId\n            ) {\n                // Unregister the recordInfo if the target controller does not\n                // match what was recorded.\n                setCommandsRecordInfo(null);\n            }\n        }\n\n        //----------------------------------------------------------------------\n        // External embedded views features\n        //----------------------------------------------------------------------\n\n        let pendingEmbeddedBlueprints = {};\n\n        /**\n         * @param {Object}\n         * @param {HTMLElement} embeddedBlueprint element to be inserted in a\n         *                      html field\n         * @param {string} model model name of the target record\n         * @param {string} field field name of the target record\n         * @param {integer} resId id of the target record\n         */\n        function setPendingEmbeddedBlueprint({embeddedBlueprint, model, field, resId}) {\n            if (!(model in pendingEmbeddedBlueprints)) {\n                pendingEmbeddedBlueprints[model] = {};\n            }\n            if (!(field in pendingEmbeddedBlueprints[model])) {\n                pendingEmbeddedBlueprints[model][field] = {};\n            }\n            pendingEmbeddedBlueprints[model][field][resId] = embeddedBlueprint;\n        }\n\n        function popPendingEmbeddedBlueprint({model, field, resId}) {\n            if (model in pendingEmbeddedBlueprints && field in pendingEmbeddedBlueprints[model]) {\n                const pendingEmbeddedBlueprint = pendingEmbeddedBlueprints[model][field][resId];\n                delete pendingEmbeddedBlueprints[model][field][resId];\n                return pendingEmbeddedBlueprint;\n            }\n        }\n\n        const knowledgeCommandsService = {\n            setCommandsRecordInfo,\n            getCommandsRecordInfo,\n            getBreadcrumbsIdentifier,\n            isRecordCompatibleWithMacro,\n            unregisterCommandsRecordInfo,\n            setPendingEmbeddedBlueprint,\n            popPendingEmbeddedBlueprint,\n        };\n        return knowledgeCommandsService;\n    }\n};\n\nregistry.category(\"services\").add(\"knowledgeCommandsService\", knowledgeCommandsService);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\n\n/**\n * This service is comprised of 2 commands that are interacting with embeddedFilters which is\n * the object that will store the different filters of the embedded views.\n *\n * @function saveFilters This function is used to save the filters for a specific embedId.\n * @function applyFilter This function adds the correct filter to the view's props.\n */\nexport const knowledgeEmbedViewsFilters = {\n    start(env) {\n        const embeddedFilters = {};\n        const commands = {\n            /**\n             * @param {*} currentController The current controller returned by the action service\n             * @param {String} filterKey The ID of the impacted embedded view\n             * @param {Object} searchModel The searchModel for the view's props that will be saved\n             * inside the embeddedFilters Object.\n             */\n            saveFilters: (currentController, filterKey, searchModel) => {\n                if (!embeddedFilters[filterKey]) {\n                    embeddedFilters[filterKey] = {};\n                }\n                embeddedFilters[filterKey][currentController.jsId] = searchModel;\n            },\n            /**\n             * This function applies the previously saved filters to the view's props.\n             * Each time that we apply filters to a view we remove the filter that we apply in order to\n             * avoid collisions and to avoid applying the filters when we change articles in Knowledge\n             * (which should not add filters to the embedded view).\n             *\n             * When we come back to the root of the breadcrumbs, we remove all the filters for the specific\n             * embed view in order to not store useless filters and to avoid collisions with filters that\n             * weren't applied to the view.\n             * This way we ensure that filters are either applied once or never, if the corresponding breadcrumb has\n             * not been opened.\n             * @param {*} currentController The current controller given by the action service\n             * @param {String} filterKey The key used to get the filters of a specific embedded view\n             * @param {*} ViewProps The embedded view's props that will be updated with the filters\n             */\n            applyFilter: (currentController, filterKey, ViewProps) => {\n                const currentJsId = currentController.jsId;\n                const embedSearchFilters = embeddedFilters[filterKey];\n\n                if (embedSearchFilters && embedSearchFilters[currentJsId]) {\n                    ViewProps.globalState.searchModel = embeddedFilters[filterKey][currentJsId];\n                    delete embeddedFilters[filterKey][currentJsId];\n                }\n\n                if (currentController.config.breadcrumbs.length === 1) {\n                    delete embeddedFilters[filterKey];\n                }\n            }\n        };\n        return commands;\n    }\n};\n\nregistry.category(\"services\").add(\"knowledgeEmbedViewsFilters\", knowledgeEmbedViewsFilters);\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { AlertDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { isVisible } from \"@web/core/utils/ui\";\nimport { Macro } from \"@web/core/macro\";\n\nclass KnowledgeMacroError extends Error {}\n\n/**\n * Abstract class for Knowledge macros, that will be used to interact like a\n * tour with a Form view chatter and/or html field.\n */\nexport class AbstractMacro extends Macro {\n    /**\n     * @param {Object} options\n     * @param {HTMLElement} options.targetXmlDoc\n     * @param {Array[Object]} options.breadcrumbs\n     * @param {Any} options.data\n     * @param {Object} options.services required: action, dialog, ui\n     */\n    constructor({ targetXmlDoc, breadcrumbs, data, services }) {\n        super({\n            name: \"restore_recort\",\n            steps: [],\n            checkDelay: 16,\n        });\n        this.targetXmlDoc = targetXmlDoc;\n        this.targetBreadcrumbs = breadcrumbs;\n        this.data = data;\n        this.services = services;\n        this.steps = this.buildSteps();\n    }\n\n    blockUI() {\n        if (!this.services.ui.isBlocked) {\n            this.services.ui.block();\n        }\n    }\n\n    buildSteps() {\n        // Build the desired macro action\n        const steps = this.getSteps();\n        if (!steps.length) {\n            return [];\n        }\n        /**\n         * Preliminary breadcrumbs macro. It will use the @see breadcrumbsIndex\n         * to switch back to the view related to the stored record\n         * (@see KnowledgeCommandsService ). Once and if the view of the target\n         * record is correctly loaded, run the specific macroAction.\n         */\n        return [\n            {\n                action: () => this.blockUI(),\n            },\n            {\n                // Restore the target Form view through its breadcrumb jsId.\n                trigger: () => {\n                    return document.querySelector(`.o_knowledge_header i.oi-chevron-left`);\n                },\n                action: async () => {\n                    try {\n                        // Try to restore the target controller.\n                        await this.services.action.restore(this.targetBreadcrumbs.at(-1).jsId);\n                    } catch {\n                        // If the controller is unreachable, abort the macro.\n                        throw new KnowledgeMacroError(\n                            _t('The record that this macro is targeting could not be found.')\n                        );\n                    }\n                },\n            },\n            {\n                // Start the requested macro when the current breadcrumbs\n                // match the target Form view.\n                trigger: () => {\n                    const controllerBreadcrumbs = this.services.action.currentController.config.breadcrumbs;\n                    if (this.targetBreadcrumbs.at(-1).jsId === controllerBreadcrumbs.at(-1)?.jsId) {\n                        return this.getFirstVisibleElement(\".o_breadcrumb .o_last_breadcrumb_item\");\n                    }\n                    return null;\n                },\n            },\n            ...steps,\n        ];\n    }\n\n    /**\n     * @param {Error} error\n     * @param {Object} step\n     * @param {integer} index\n     */\n    onError(error, step, index) {\n        this.blockUI();\n        if (error instanceof KnowledgeMacroError) {\n            this.services.dialog.add(AlertDialog, {\n                body: error.message,\n                title: _t(\"Error\"),\n                confirmLabel: _t(\"Close\"),\n            });\n        } else {\n            console.error(error);\n        }\n    }\n\n    onTimeout() {\n        throw new KnowledgeMacroError(_t(\"The operation could not be completed.\"));\n    }\n\n    /**\n     * Searches for the first element in the dom matching the selector. The\n     * results are filtered with `filter` and the returned element is either\n     * the first or the last depending on `reverse`.\n     *\n     * @param {String|HTMLElement} selector\n     * @param {Function} filter\n     * @param {boolean} reverse\n     * @returns {HTMLElement}\n     */\n    getFirstVisibleElement(selector, filter=false, reverse=false) {\n        const elementsArray = typeof(selector) === 'string' ? Array.from(document.querySelectorAll(selector)) : [selector];\n        const sel = filter ? elementsArray.filter(filter) : elementsArray;\n        for (let i = 0; i < sel.length; i++) {\n            i = reverse ? sel.length - 1 - i : i;\n            if (isVisible(sel[i])) {\n                return sel[i];\n            }\n        }\n        return null;\n    }\n\n    /**\n     * To be overridden by an actual Macro implementation. It should contain\n     * the steps to be executed on the target Form view.\n     *\n     * @returns {Array[Object]}\n     */\n    getSteps() {\n        return [];\n    }\n\n    unblockUI() {\n        if (this.services.ui.isBlocked) {\n            this.services.ui.unblock();\n        }\n    }\n\n    /**\n     * Validate that the macro is still on the correct Form view by checking\n     * that the target breadcrumbs are the same as the current ones. To be used\n     * at each step inside the target Form view. Throwing an error will\n     * terminate the macro.\n     */\n    validatePage() {\n        const controllerBreadcrumbs = this.services.action.currentController.config.breadcrumbs;\n        if (this.targetBreadcrumbs.at(-1).jsId !== controllerBreadcrumbs.at(-1)?.jsId) {\n            throw new KnowledgeMacroError(\n                _t('The record that this macro is targeting could not be found.')\n            );\n        }\n    }\n    /**\n     * Handle the case where an item is hidden in a tab of the form view\n     * notebook. Only pages with the \"name\" attribute set can be navigated to.\n     * Other pages are ignored (and the fields they contain are too).\n     * @see FormControllerPatch\n     *\n     * @param {String} targetSelector selector (will be used in the target\n     * xml document) for the element (likely an html field) that could be\n     * hidden inside a non-active tab of the notebook.\n     */\n    searchInXmlDocNotebookTab(targetSelector) {\n        const searchElement = this.targetXmlDoc.querySelector(targetSelector);\n        const page = searchElement ? searchElement.closest('page') : undefined;\n        const pageName = page ? page.getAttribute('name') : undefined;\n        if (!pageName) {\n            return;\n        }\n        const pageEl = this.getFirstVisibleElement(`.o_notebook .nav-link[name=${pageName}]:not(.active)`);\n        if (pageEl) {\n            pageEl.click();\n        }\n    }\n}\n", "/** @odoo-module */\n\nimport { AbstractMacro } from \"@knowledge/macros/abstract_macro\";\nimport { click } from \"@odoo/hoot-dom\";\nimport {\n    pasteElements,\n    replaceHtmlFieldContentWith\n} from \"@knowledge/macros/utils\";\n\n/**\n * Macro that will open the Full Composer Form view dialog in the Form view\n * context of a target record, and paste text content inside it. Does not\n * actually send the message.\n */\nexport class SendAsMessageMacro extends AbstractMacro {\n    /**\n     * @override\n     * @returns {Array[Object]}\n     */\n    getSteps() {\n        let sendMessageLastClickedEl = null;\n        return [\n            {\n                // Search for the chatter button to send a message and make sure\n                // that the composer is visible. Search in notebook tabs too.\n                trigger: () => {\n                    this.validatePage();\n                    const el = this.getFirstVisibleElement(\n                        \".o-mail-Chatter-sendMessage:not([disabled])\"\n                    );\n                    if (el) {\n                        if (el.classList.contains(\"active\")) {\n                            return el;\n                        } else if (el !== sendMessageLastClickedEl) {\n                            el.click();\n                            sendMessageLastClickedEl = el;\n                        }\n                    } else {\n                        this.searchInXmlDocNotebookTab(\"chatter\");\n                    }\n                    return null;\n                },\n            },\n            {\n                // Open the full composer Form view Dialog.\n                trigger: () => {\n                    this.validatePage();\n                    return this.getFirstVisibleElement(\n                        \".o-mail-Composer-fullComposer:not([disabled])\"\n                    );\n                },\n                async action(trigger) {\n                    await click(trigger);\n                },\n            },\n            {\n                // Paste the html data inside the message body.\n                trigger: () => {\n                    this.validatePage();\n                    const dialog = this.getFirstVisibleElement(\".o_dialog .o_mail_composer_form\");\n                    if (dialog) {\n                        return this.getFirstVisibleElement(\n                            dialog.querySelector('.o_field_html[name=\"body\"] .odoo-editor-editable')\n                        );\n                    }\n                    return null;\n                },\n                action: (el) => {\n                    replaceHtmlFieldContentWith(this.data.dataTransfer, el);\n                },\n            },\n            {\n                action: () => this.unblockUI(),\n            },\n        ];\n    }\n}\n\n/**\n * Macro that will append content in the target record's html field in its Form\n * view. Does not trigger the save (field will be dirty).\n */\nexport class UseAsDescriptionMacro extends AbstractMacro {\n    /**\n     * @override\n     * @returns {Array[Object]}\n     */\n    getSteps() {\n        return [\n            {\n                // Ensure that the Form view is editable\n                trigger: () => this.getFirstVisibleElement(\".o_form_editable\"),\n            },\n            {\n                // Search for the target html field and ensure that it is editable.\n                // Search in notebook tabs too.\n                trigger: () => {\n                    this.validatePage();\n                    const el = this.getFirstVisibleElement(\n                        `.o_field_html[name=\"${this.data.fieldName}\"]`,\n                        (element) => element.querySelector(\".odoo-editor-editable\")\n                    );\n                    if (el) {\n                        return el;\n                    }\n                    if (this.data.pageName) {\n                        this.searchInXmlDocNotebookTab(`page[name=\"${this.data.pageName}\"]`);\n                    }\n                    return null;\n                },\n                async action(trigger) {\n                    await click(trigger);\n                },\n            },\n            {\n                // Search for the editable element. Paste the html data inside the\n                // field.\n                trigger: () => {\n                    this.validatePage();\n                    return this.getFirstVisibleElement(\n                        `.o_field_html[name=\"${this.data.fieldName}\"] .odoo-editor-editable`\n                    );\n                },\n                action: (el) => {\n                    el.addEventListener(\n                        \"onHistoryResetFromPeer\",\n                        (ev) => pasteElements(this.data.dataTransfer, el),\n                        { once: true }\n                    );\n                    pasteElements(this.data.dataTransfer, el);\n                },\n            },\n            {\n                action: () => this.unblockUI(),\n            },\n        ];\n    }\n}\n", "/** @odoo-module */\n\nimport { AbstractMacro } from \"@knowledge/macros/abstract_macro\";\nimport { dragAndDrop } from \"@knowledge/macros/utils\";\n\n/**\n * Macro that will add a file as an attachment of a record and display it\n * in its Form view.\n */\nexport class UseAsAttachmentMacro extends AbstractMacro {\n    /**\n     * @override\n     * @returns {Array[Object]}\n     */\n    getSteps() {\n        let attachFilesLastClickedEl = null;\n        return [\n            {\n                // Search for the chatter button to attach a file and open\n                // the AttachmentList zone. Search in notebook tabs too.\n                trigger: () => {\n                    this.validatePage();\n                    const el = this.getFirstVisibleElement(\n                        \".o-mail-Chatter-attachFiles:not([disabled])\",\n                        (matchEl) => {\n                            // Wait for the attachments to be loaded by the chatter.\n                            const attachmentsCountEl = matchEl.querySelector(\"sup\");\n                            return (\n                                attachmentsCountEl &&\n                                Number.parseInt(attachmentsCountEl.textContent) > 0\n                            );\n                        }\n                    );\n                    if (el) {\n                        const attachmentBoxEl = this.getFirstVisibleElement(\n                            \".o-mail-AttachmentBox .o-mail-AttachmentList\"\n                        );\n                        if (attachmentBoxEl) {\n                            return attachmentBoxEl;\n                        } else if (el !== attachFilesLastClickedEl) {\n                            el.click();\n                            attachFilesLastClickedEl = el;\n                        }\n                    } else {\n                        this.searchInXmlDocNotebookTab(\"chatter\");\n                    }\n                    return null;\n                },\n                action: (el) => el.scrollIntoView(),\n            },\n            {\n                action: () => this.unblockUI(),\n            },\n        ];\n    }\n}\n\n/**\n * Macro that will add a file to a message (without sending it) in the context\n * of a record in its Form view.\n */\nexport class AttachToMessageMacro extends AbstractMacro {\n    /**\n     * @override\n     * @returns {Array[Object]}\n     */\n    getSteps() {\n        let sendMessageLastClickedEl = null;\n        return [\n            {\n                // Search for the chatter button to send a message and make sure\n                // that the composer is visible. Search in notebook tabs too.\n                trigger: () => {\n                    this.validatePage();\n                    const el = this.getFirstVisibleElement(\n                        \".o-mail-Chatter-sendMessage:not([disabled])\"\n                    );\n                    if (el) {\n                        if (el.classList.contains(\"active\")) {\n                            return el;\n                        } else if (el !== sendMessageLastClickedEl) {\n                            el.click();\n                            sendMessageLastClickedEl = el;\n                        }\n                    } else {\n                        this.searchInXmlDocNotebookTab(\"chatter\");\n                    }\n                    return null;\n                },\n                action: (el) => {\n                    el.scrollIntoView();\n                },\n            },\n            {\n                // Search for the composer button to attach files and start dragging\n                // the data file over it.\n                trigger: () => {\n                    this.validatePage();\n                    return this.getFirstVisibleElement(\n                        \".o-mail-Composer-attachFiles:not([disabled])\"\n                    );\n                },\n                action: dragAndDrop.bind(this, \"dragenter\", this.data.dataTransfer),\n            },\n            {\n                // Search for the composer drop zone for attachments and drop the\n                // data file into it.\n                trigger: () => {\n                    this.validatePage();\n                    return this.getFirstVisibleElement(\".o-mail-Composer-dropzone\");\n                },\n                action: dragAndDrop.bind(this, \"drop\", this.data.dataTransfer),\n            },\n            {\n                action: () => this.unblockUI(),\n            },\n        ];\n    }\n}\n", "/** @odoo-module */\n\n/**\n * @param {String} string - Name of the drag event\n * @param {DataTransfer} dataTransfer - Object used to store data\n * @param {HTMLElement} target - Target element\n */\nexport function dragAndDrop(type, dataTransfer, target) {\n    const fakeDragAndDrop = new Event(type, {\n        bubbles: true,\n        cancelable: true,\n        composed: true,\n    });\n    fakeDragAndDrop.dataTransfer = dataTransfer;\n    target.dispatchEvent(fakeDragAndDrop);\n}\n\n/**\n * @param {DataTransfer} dataTransfer - Object used to store data\n * @param {HTMLElement} target - Target element\n */\nexport function pasteElements(dataTransfer, target) {\n    const fakePaste = new Event('paste', {\n        bubbles: true,\n        cancelable: true,\n        composed: true,\n    });\n    fakePaste.clipboardData = dataTransfer;\n\n    const sel = document.getSelection();\n    sel.removeAllRanges();\n    const range = document.createRange();\n    const lastChild = target.lastChild;\n    if (!lastChild) {\n        range.setStart(target, 0);\n        range.setEnd(target, 0);\n    } else {\n        const subLastChild = lastChild.lastChild;\n        if (subLastChild) {\n            if (subLastChild.nodeType === Node.ELEMENT_NODE && subLastChild.tagName === 'BR') {\n                range.setStartBefore(subLastChild);\n                range.setEndBefore(subLastChild);\n            } else {\n                range.setStartAfter(subLastChild);\n                range.setEndAfter(subLastChild);\n            }\n        } else {\n            range.setStartAfter(lastChild);\n            range.setEndAfter(lastChild);\n        }\n    }\n    const lastElementChild = target.lastElementChild;\n    if (lastElementChild) {\n        lastElementChild.scrollIntoView();\n    } else {\n        target.scrollIntoView();\n    }\n    sel.addRange(range);\n    target.dispatchEvent(fakePaste);\n}\n\n/**\n * @param {DataTransfer} dataTransfer\n * @param {HTMLElement} editable\n */\nexport function replaceHtmlFieldContentWith(dataTransfer, editable) {\n    editable.replaceChildren(); // Hack to avoid having a paragraph after the user's signature\n    const event = new Event(\"paste\", {\n        bubbles: true,\n        cancelable: true,\n        composed: true,\n    });\n    event.clipboardData = dataTransfer;\n    editable.dispatchEvent(event);\n}\n", "!function(e,t){\"object\"==typeof exports&&\"object\"==typeof module?module.exports=t():\"function\"==typeof define&&define.amd?define([],t):\"object\"==typeof exports?exports.printJS=t():e.printJS=t()}(window,(function(){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){\"undefined\"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:\"Module\"}),Object.defineProperty(e,\"__esModule\",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&\"object\"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,\"default\",{enumerable:!0,value:e}),2&t&&\"string\"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e[\"default\"]}:function(){return e};return n.d(t,\"a\",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p=\"\",n(n.s=0)}([function(e,t,n){e.exports=n(2)},function(e,t,n){},function(e,t,n){\"use strict\";n.r(t);n(1);var r={isFirefox:function(){return\"undefined\"!=typeof InstallTrigger},getFirefoxMajorVersion:function(e){var t=(e=e||navigator.userAgent).toLowerCase().match(/firefox\\/(\\S+)/);if(t)return t[1].split(\".\").map((function(e){return parseInt(e)}))[0]},isIE:function(){return-1!==navigator.userAgent.indexOf(\"MSIE\")||!!document.documentMode},isEdge:function(){return!r.isIE()&&!!window.StyleMedia},isChrome:function(){return!!(arguments.length>0&&arguments[0]!==undefined?arguments[0]:window).chrome},isSafari:function(){return Object.prototype.toString.call(window.HTMLElement).indexOf(\"Constructor\")>0||-1!==navigator.userAgent.toLowerCase().indexOf(\"safari\")},isIOSChrome:function(){return-1!==navigator.userAgent.toLowerCase().indexOf(\"crios\")}},o=r,i={show:function(e){var t=document.createElement(\"div\");t.setAttribute(\"style\",\"font-family:sans-serif; display:table; text-align:center; font-weight:300; font-size:30px; left:0; top:0;position:fixed; z-index: 9990;color: #0460B5; width: 100%; height: 100%; background-color:rgba(255,255,255,.9);transition: opacity .3s ease;\"),t.setAttribute(\"id\",\"printJS-Modal\");var n=document.createElement(\"div\");n.setAttribute(\"style\",\"display:table-cell; vertical-align:middle; padding-bottom:100px;\");var r=document.createElement(\"div\");r.setAttribute(\"class\",\"printClose\"),r.setAttribute(\"id\",\"printClose\"),n.appendChild(r);var o=document.createElement(\"span\");o.setAttribute(\"class\",\"printSpinner\"),n.appendChild(o);var a=document.createTextNode(e.modalMessage);n.appendChild(a),t.appendChild(n),document.getElementsByTagName(\"body\")[0].appendChild(t),document.getElementById(\"printClose\").addEventListener(\"click\",(function(){i.close()}))},close:function(){var e=document.getElementById(\"printJS-Modal\");e&&e.parentNode.removeChild(e)}},a=i;function l(e){return l=\"function\"==typeof Symbol&&\"symbol\"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&\"function\"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?\"symbol\":typeof e},l(e)}function d(e){return e.charAt(0).toUpperCase()+e.slice(1)}function c(e,t){for(var n=0;n<e.length;n++)if(\"object\"===l(t)&&-1!==t.indexOf(e[n]))return!0;return!1}function p(e,t){var n=document.createElement(\"div\");if(u(t.header))n.innerHTML=t.header;else{var r=document.createElement(\"h1\"),o=document.createTextNode(t.header);r.appendChild(o),r.setAttribute(\"style\",t.headerStyle),n.appendChild(r)}e.insertBefore(n,e.childNodes[0])}function s(e,t){var n=document.createElement(\"div\");if(u(t.footer))n.innerHTML=t.footer;else{var r=document.createElement(\"h1\"),o=document.createTextNode(t.footer);r.appendChild(o),r.setAttribute(\"style\",t.footerStyle),n.appendChild(r)}e.insertBefore(n,e.childNodes.lastChild)}function f(e){e.showModal&&a.close(),e.onLoadingEnd&&e.onLoadingEnd(),(e.showModal||e.onLoadingStart)&&window.URL.revokeObjectURL(e.printable);var t=\"mouseover\";(o.isChrome()||o.isFirefox())&&(t=\"focus\");window.addEventListener(t,(function n(){window.removeEventListener(t,n),e.onPrintDialogClose();var r=document.getElementById(e.frameId);r&&(e.frameRemoveDelay?setTimeout((function(){r.remove()}),e.frameRemoveDelay):r.remove())}))}function u(e){return new RegExp(\"<([A-Za-z][A-Za-z0-9]*)\\\\b[^>]*>(.*?)</\\\\1>\").test(e)}function m(e,t){try{if(e.focus(),o.isEdge()||o.isIE())try{e.contentWindow.document.execCommand(\"print\",!1,null)}catch(n){setTimeout((function(){e.contentWindow.print()}),1e3)}else setTimeout((function(){e.contentWindow.print()}),1e3)}catch(r){t.onError(r)}finally{o.isFirefox()&&o.getFirefoxMajorVersion()<110&&(e.style.visibility=\"hidden\",e.style.left=\"-1px\"),f(t)}}var b={send:function(e,t){document.getElementsByTagName(\"body\")[0].appendChild(t);var n=document.getElementById(e.frameId);n.onload=function(){if(\"pdf\"!==e.type){var t=n.contentWindow||n.contentDocument;if(t.document&&(t=t.document),t.body.appendChild(e.printableElement),\"pdf\"!==e.type&&e.style){var r=document.createElement(\"style\");r.innerHTML=e.style,t.head.appendChild(r)}var i=t.getElementsByTagName(\"img\");i.length>0?function(e){var t=e.map((function(e){if(e.src&&e.src!==window.location.href)return function(e){return new Promise((function(t){(function n(){e&&\"undefined\"!=typeof e.naturalWidth&&0!==e.naturalWidth&&e.complete?t():setTimeout(n,500)})()}))}(e)}));return Promise.all(t)}(Array.from(i)).then((function(){return m(n,e)})):m(n,e)}else o.isFirefox()&&o.getFirefoxMajorVersion()<110?setTimeout((function(){return m(n,e)}),1e3):m(n,e)}}},y={print:function(e,t){if(e.base64){-1!==e.printable.indexOf(\",\")&&(e.printable=e.printable.split(\",\")[1]);var n=Uint8Array.from(atob(e.printable),(function(e){return e.charCodeAt(0)}));h(e,t,n)}else{e.printable=/^(blob|http|\\/\\/)/i.test(e.printable)?e.printable:window.location.origin+(\"/\"!==e.printable.charAt(0)?\"/\"+e.printable:e.printable);var r=new window.XMLHttpRequest;r.responseType=\"arraybuffer\",r.addEventListener(\"error\",(function(){f(e),e.onError(r.statusText,r)})),r.addEventListener(\"load\",(function(){if(-1===[200,201].indexOf(r.status))return f(e),void e.onError(r.statusText,r);h(e,t,r.response)})),r.open(\"GET\",e.printable,!0),r.send()}}};function h(e,t,n){var r=new window.Blob([n],{type:\"application/pdf\"});r=window.URL.createObjectURL(r),t.setAttribute(\"src\",r),b.send(e,t)}function g(e){return g=\"function\"==typeof Symbol&&\"symbol\"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&\"function\"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?\"symbol\":typeof e},g(e)}var v={print:function(e,t){var n,r=\"object\"===g(n=e.printable)&&n&&(n instanceof HTMLElement||1===n.nodeType)?e.printable:document.getElementById(e.printable);r?(e.printableElement=w(r,e),e.header&&p(e.printableElement,e),e.footer&&s(e.printableElement,e),b.send(e,t)):window.console.error(\"Invalid HTML element id: \"+e.printable)}};function w(e,t){for(var n=e.cloneNode(),r=Array.prototype.slice.call(e.childNodes),o=0;o<r.length;o++)if(-1===t.ignoreElements.indexOf(r[o].id)){var i=w(r[o],t);n.appendChild(i)}switch(t.scanStyles&&1===e.nodeType&&n.setAttribute(\"style\",function(e,t){for(var n=\"\",r=(document.defaultView||window).getComputedStyle(e,\"\"),o=0;o<r.length;o++)(-1!==t.targetStyles.indexOf(\"*\")||-1!==t.targetStyle.indexOf(r[o])||c(t.targetStyles,r[o]))&&r.getPropertyValue(r[o])&&(n+=r[o]+\":\"+r.getPropertyValue(r[o])+\";\");return n+\"max-width: \"+t.maxWidth+\"px !important; font-size: \"+t.font_size+\" !important;\"}(e,t)),e.tagName){case\"SELECT\":n.value=e.value;break;case\"CANVAS\":n.getContext(\"2d\").drawImage(e,0,0)}return n}var E={print:function(e,t){e.printableElement=document.createElement(\"div\"),e.printableElement.setAttribute(\"style\",\"width:100%\"),e.printableElement.innerHTML=e.printable,e.header&&p(e.printableElement,e),e.footer&&s(e.printableElement,e),b.send(e,t)}},S={print:function(e,t){e.printable.constructor!==Array&&(e.printable=[e.printable]),e.printableElement=document.createElement(\"div\"),e.printable.forEach((function(t){var n=document.createElement(\"img\");if(n.setAttribute(\"style\",e.imageStyle),n.src=t,o.isFirefox()){var r=n.src;n.src=r}var i=document.createElement(\"div\");i.appendChild(n),e.printableElement.appendChild(i)})),e.header&&p(e.printableElement,e),e.footer&&s(e.printableElement,e),b.send(e,t)}};function x(e){return x=\"function\"==typeof Symbol&&\"symbol\"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&\"function\"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?\"symbol\":typeof e},x(e)}var C={print:function(e,t){if(\"object\"!==x(e.printable))throw new Error(\"Invalid javascript data object (JSON).\");if(\"boolean\"!=typeof e.repeatTableHeader)throw new Error(\"Invalid value for repeatTableHeader attribute (JSON).\");if(!e.properties||!Array.isArray(e.properties))throw new Error(\"Invalid properties array for your JSON data.\");e.properties=e.properties.map((function(t){return{field:\"object\"===x(t)?t.field:t,displayName:\"object\"===x(t)?t.displayName:t,columnSize:\"object\"===x(t)&&t.columnSize?t.columnSize+\";\":100/e.properties.length+\"%;\"}})),e.printableElement=document.createElement(\"div\"),e.header&&p(e.printableElement,e),e.printableElement.innerHTML+=function(e){var t=e.printable,n=e.properties,r='<table style=\"border-collapse: collapse; width: 100%;\">';e.repeatTableHeader&&(r+=\"<thead>\");r+=\"<tr>\";for(var o=0;o<n.length;o++)r+='<th style=\"width:'+n[o].columnSize+\";\"+e.gridHeaderStyle+'\">'+d(n[o].displayName)+\"</th>\";r+=\"</tr>\",e.repeatTableHeader&&(r+=\"</thead>\");r+=\"<tbody>\";for(var i=0;i<t.length;i++){r+=\"<tr>\";for(var a=0;a<n.length;a++){var l=t[i],c=n[a].field.split(\".\");if(c.length>1)for(var p=0;p<c.length;p++)l=l[c[p]];else l=l[n[a].field];r+='<td style=\"width:'+n[a].columnSize+e.gridStyle+'\">'+l+\"</td>\"}r+=\"</tr>\"}return r+=\"</tbody></table>\"}(e),e.footer&&s(e.printableElement,e),b.send(e,t)}};function T(e){return T=\"function\"==typeof Symbol&&\"symbol\"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&\"function\"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?\"symbol\":typeof e},T(e)}var A=[\"pdf\",\"html\",\"image\",\"json\",\"raw-html\"],L=function(){var e={printable:null,fallbackPrintable:null,type:\"pdf\",header:null,headerStyle:\"font-weight: 300;\",footer:null,footerStyle:\"font-weight: 300;\",maxWidth:800,properties:null,gridHeaderStyle:\"font-weight: bold; padding: 5px; border: 1px solid #dddddd;\",gridStyle:\"border: 1px solid lightgray; margin-bottom: -1px;\",showModal:!1,onError:function(e){throw e},onLoadingStart:null,onLoadingEnd:null,onPrintDialogClose:function(){},onIncompatibleBrowser:function(){},modalMessage:\"Retrieving Document...\",frameId:\"printJS\",frameRemoveDelay:null,printableElement:null,documentTitle:\"Document\",targetStyle:[\"clear\",\"display\",\"width\",\"min-width\",\"height\",\"min-height\",\"max-height\"],targetStyles:[\"border\",\"box\",\"break\",\"text-decoration\"],ignoreElements:[],repeatTableHeader:!0,css:null,style:null,scanStyles:!0,base64:!1,onPdfOpen:null,font:\"TimesNewRoman\",font_size:\"12pt\",honorMarginPadding:!0,honorColor:!1,imageStyle:\"max-width: 100%;\"},t=arguments[0];if(t===undefined)throw new Error(\"printJS expects at least 1 attribute.\");switch(T(t)){case\"string\":e.printable=encodeURI(t),e.fallbackPrintable=e.printable,e.type=arguments[1]||e.type;break;case\"object\":for(var n in e.printable=t.printable,e.fallbackPrintable=\"undefined\"!=typeof t.fallbackPrintable?t.fallbackPrintable:e.printable,e.fallbackPrintable=e.base64?\"data:application/pdf;base64,\".concat(e.fallbackPrintable):e.fallbackPrintable,e)\"printable\"!==n&&\"fallbackPrintable\"!==n&&(e[n]=\"undefined\"!=typeof t[n]?t[n]:e[n]);break;default:throw new Error('Unexpected argument type! Expected \"string\" or \"object\", got '+T(t))}if(!e.printable)throw new Error(\"Missing printable information.\");if(!e.type||\"string\"!=typeof e.type||-1===A.indexOf(e.type.toLowerCase()))throw new Error(\"Invalid print type. Available types are: pdf, html, image and json.\");e.showModal&&a.show(e),e.onLoadingStart&&e.onLoadingStart();var r=document.getElementById(e.frameId);r&&r.parentNode.removeChild(r);var i=document.createElement(\"iframe\");switch(o.isFirefox()?i.setAttribute(\"style\",\"width: 1px; height: 100px; position: fixed; left: 0; top: 0; opacity: 0; border-width: 0; margin: 0; padding: 0\"):i.setAttribute(\"style\",\"visibility: hidden; height: 0; width: 0; position: absolute; border: 0\"),i.setAttribute(\"id\",e.frameId),\"pdf\"!==e.type&&(i.srcdoc=\"<html><head><title>\"+e.documentTitle+\"</title>\",e.css&&(Array.isArray(e.css)||(e.css=[e.css]),e.css.forEach((function(e){i.srcdoc+='<link rel=\"stylesheet\" href=\"'+e+'\">'}))),i.srcdoc+=\"</head><body></body></html>\"),e.type){case\"pdf\":if(o.isIE())try{console.info(\"Print.js doesn't support PDF printing in Internet Explorer.\"),window.open(e.fallbackPrintable,\"_blank\").focus(),e.onIncompatibleBrowser()}catch(l){e.onError(l)}finally{e.showModal&&a.close(),e.onLoadingEnd&&e.onLoadingEnd()}else y.print(e,i);break;case\"image\":S.print(e,i);break;case\"html\":v.print(e,i);break;case\"raw-html\":E.print(e,i);break;case\"json\":C.print(e,i)}},j=L;\"undefined\"!=typeof window&&(window.printJS=j);t[\"default\"]=j}])[\"default\"]}));\n//# sourceMappingURL=print.map", "/** @odoo-module **/\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\n\n/**\n * Generates the report url given a report action.\n *\n * @private\n * @param {ReportAction} action\n * @param {ReportType} type\n * @returns {string}\n */\nfunction _getReportUrl(action, type, env) {\n    let url = `/report/${type}/${action.report_name}`;\n    \n    const contextDict = Object.assign(\n        {}, \n        action.context || {},\n        env && env.services && env.services.user ? env.services.user.context : {}\n    );\n\n    if (action.data && JSON.stringify(action.data) !== \"{}\") {\n        const options = encodeURIComponent(JSON.stringify(action.data));\n        const context = encodeURIComponent(JSON.stringify(contextDict));\n        url += `?options=${options}&context=${context}`;\n    } else {\n        if (contextDict.active_ids) {\n            url += `/${contextDict.active_ids.join(\",\")}`;\n        }\n        const context = encodeURIComponent(JSON.stringify(contextDict));\n        url += `?context=${context}`;\n    }\n    return url;\n}\n\n/**\n * Launches download action of the report\n *\n * @private\n * @param {ReportAction} action\n * @param {ActionOptions} options\n * @returns {Promise}\n */\nasync function _openPrintDialog(action, options, env, type) {\n    const url = _getReportUrl(action, type, env);\n    printJS({\n        printable: url,\n        showModal: true,\n        type: 'pdf',\n        onPrintDialogClose: function () {\n            const onClose = options.onClose;\n            if (action.close_on_report_download) {\n                return env.services.action.doAction({ type: \"ir.actions.act_window_close\" }, { onClose });\n            } else if (onClose) {\n                onClose();\n            }\n        },\n    });\n    return Promise.resolve(true)\n\n}\n\nlet wkhtmltopdfStateProm;\n\nregistry.category(\"ir.actions.report handlers\").add(\"open_print_dialog_handler\", async (action, options, env) => {\n    let is_open_browser_dialog = false\n    if (action.printing_action && action.printing_action == 'open_print_dialog') {\n        is_open_browser_dialog = true\n    }\n    else {\n        is_open_browser_dialog = await rpc(\"/report/is_open_print_dialog\", {report_ref: (action.id) ? action.id : action.report_name});\n    }\n    if (!is_open_browser_dialog)\n        return Promise.resolve(false);\n\n    if (action.report_type === \"qweb-pdf\") {\n        // check the state of wkhtmltopdf before proceeding\n        if (!wkhtmltopdfStateProm) {\n            wkhtmltopdfStateProm = rpc(\"/report/check_wkhtmltopdf\");\n        }\n        const state = await wkhtmltopdfStateProm;\n        // display a notification according to wkhtmltopdf's state\n        const link = '<br><br><a href=\"http://wkhtmltopdf.org/\" target=\"_blank\">wkhtmltopdf.org</a>';\n        const WKHTMLTOPDF_MESSAGES = {\n            broken:\n                _t(\n                    \"Your installation of Wkhtmltopdf seems to be broken. The report will be shown \" +\n                        \"in html.\"\n                ) + link,\n            install:\n                _t(\n                    \"Unable to find Wkhtmltopdf on this system. The report will be shown in \" + \"html.\"\n                ) + link,\n            upgrade:\n                _t(\n                    \"You should upgrade your version of Wkhtmltopdf to at least 0.12.0 in order to \" +\n                        \"get a correct display of headers and footers as well as support for \" +\n                        \"table-breaking between pages.\"\n                ) + link,\n            workers: _t(\n                \"You need to start Odoo with at least two workers to print a pdf version of \" +\n                    \"the reports.\"\n            ),\n        };        \n        if (state in WKHTMLTOPDF_MESSAGES) {\n            env.services.notification.add(WKHTMLTOPDF_MESSAGES[state], {\n                sticky: true,\n                title: _t(\"Report\"),\n            });\n        }\n        if (state === \"upgrade\" || state === \"ok\") {\n            // trigger the download of the PDF report\n            return _openPrintDialog(action, options, env, \"pdf\");\n        } else {\n            // open the report in the client action if generating the PDF is not possible\n            return Promise.resolve(false);\n        }\n    } else if (action.report_type === \"qweb-text\") {\n        return _openPrintDialog(action, options, env, \"text\");\n    }\n});\n", "import { DateTimeField, dateTimeField } from '@web/views/fields/datetime/datetime_field';\nimport { formatDateTime } from '@web/core/l10n/dates';\nimport { localization } from \"@web/core/l10n/localization\";\nimport { registry } from \"@web/core/registry\";\n\nclass DateTimeNoSeconds extends DateTimeField {\n    static template = \"sign.DateTimeNoSeconds\";\n    /**\n     * @override\n     * @returns { Date }\n     */\n    getFormattedValue() {\n        const timeFormat = localization.dateTimeFormat.replace(\":ss\", \"\");\n        const { data } = this.props.record;\n        return formatDateTime(data.create_date, { format: timeFormat });\n    }\n}\n\nexport const dateTimeNoSeconds = {\n    ...dateTimeField,\n    component: DateTimeNoSeconds,\n}\n\nregistry.category(\"fields\").add('datetime_no_seconds', dateTimeNoSeconds);\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(\"sign_tour\", {\n    url: \"/odoo\",\n    steps: () => [\n        stepUtils.showAppsMenuItem(),\n        {\n            trigger: '.o_app[data-menu-xmlid=\"sign.menu_document\"]',\n            content: markup(_t(\"Let's <b>prepare & sign</b> our first document.\")),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_nocontent_help .o_sign_sample\",\n            content: _t(\"Try out this sample contract.\"),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \":iframe .o_sign_field_type_button:contains(\" + _t(\"Signature\") + \")\",\n            content: markup(_t(\"<b>Drag & drop \u201cSignature\u201d</b> into the bottom of the document.\")),\n            tooltipPosition: \"bottom\",\n            run: \"drag_and_drop :iframe #viewer\",\n        },\n        {\n            trigger: \".o_control_panel .o_sign_template_send\",\n            content: markup(\n                _t(\n                    \"Well done, your document is ready!<br>Let's send it to get our first signature.\"\n                )\n            ),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_field_signer_x2many\",\n            content: markup(\n                _t(\n                    \"Select the contact who should sign, according to their role.<br>In this example, select your own contact to sign the document yourself.\"\n                )\n            ),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: 'button[name=\"send_request\"]',\n            content: _t(\"Let's send the request by email.\"),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_control_panel .o_sign_sign_directly\",\n            content: markup(\n                _t(\n                    \"Since you're the one signing this document, you can do it directly within Odoo.<br>External users can use the link provided by email.\"\n                )\n            ),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \":iframe .o_sign_sign_item_navigator\",\n            content: _t(\"Follow the guide to sign the document.\"),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger:\n                \":iframe .o_sign_sign_item_navigator, :iframe .o_sign_sign_item[data-signature]\",\n            content: markup(\n                _t(\n                    \"Draw your most beautiful signature!<br>You can also create one automatically or load a signature from your computer.\"\n                )\n            ),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \"footer.modal-footer button.btn-primary:enabled\",\n            content: _t(\"Nearly there, keep going!\"),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \":iframe body:not(:has(footer.modal-footer button.btn-primary))\",\n        },\n        {\n            trigger: \".o_sign_validate_banner button.o_validate_button\",\n            content: _t(\"Congrats, your signature is ready to be submitted!\"),\n            tooltipPosition: \"top\",\n            run: \"click\",\n        },\n        {\n            trigger: '.modal-dialog button:contains(\"' + _t(\"Close\") + '\")',\n            content: markup(\n                _t(\n                    \"That's it, all done!<br>The document is signed, and a copy has been sent by email to all participants, along with a traceability report.\"\n                )\n            ),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n    ],\n});\n", "import { ActivityListPopoverItem } from \"@mail/core/web/activity_list_popover_item\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(ActivityListPopoverItem.prototype, {\n    get hasMarkDoneButton() {\n        return super.hasMarkDoneButton && this.props.activity.activity_category !== \"sign_request\";\n    },\n\n    async onClickRequestSign() {\n        const { res_model, res_id } = this.props;\n        const documentReference = res_model && res_id ? `${res_model},${res_id}` : false;\n        await this.props.activity.requestSignature(this.props.onActivityChanged, documentReference);\n    },\n});\n", "import { Activity } from \"@mail/core/web/activity_model\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { _t } from \"@web/core/l10n/translation\";\n\npatch(Activity.prototype, {\n    requestSignature(onClose = () => {}, documentReference = false) {\n        const additionalContext = {\n            sign_directly_without_mail: false,\n            default_activity_id: this.id,\n        };\n        if (documentReference) {\n            additionalContext.default_reference_doc = documentReference;\n        }\n        return this.store.env.services.action.doAction(\n            {\n                name: _t(\"Signature Request\"),\n                type: \"ir.actions.act_window\",\n                view_mode: \"form\",\n                views: [[false, \"form\"]],\n                target: \"new\",\n                res_model: \"sign.send.request\",\n            },\n            { additionalContext, onClose }\n        );\n    },\n});\n", "import { Activity } from \"@mail/core/web/activity\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Activity.prototype, {\n    async onClickRequestSign() {\n        const { res_model, res_id } = this.props.activity;\n        const documentReference = res_model && res_id ? `${res_model},${res_id}` : false;\n        await this.props.activity.requestSignature(\n            this.props.reloadParentView,\n            documentReference\n        );\n    },\n});\n", "import { useRef, useEffect, useState } from \"@odoo/owl\";\n\nexport const SignDocumentDropZone = (T) => class SignDocumentDropZone extends T {\n    setup() {\n        super.setup();\n        this.root = useRef(\"root\");\n        this.dragState = useState({\n            showDragZone: false,\n        });\n        useEffect(\n            (el) => {\n                if (!el) {\n                    return;\n                }\n                const highlight = this.highlight.bind(this);\n                const unhighlight = this.unhighlight.bind(this);\n                const drop = this.onDrop.bind(this);\n                el.addEventListener(\"dragover\", highlight);\n                el.addEventListener(\"dragleave\", unhighlight);\n                el.addEventListener(\"drop\", drop);\n                return () => {\n                    el.removeEventListener(\"dragover\", highlight);\n                    el.removeEventListener(\"dragleave\", unhighlight);\n                    el.removeEventListener(\"drop\", drop);\n                };\n            },\n            () => [document.querySelector('.o_content')]\n        );\n    }\n\n    highlight(ev) {\n        ev.stopPropagation();\n        ev.preventDefault();\n        this.dragState.showDragZone = true;\n    }\n\n    unhighlight(ev) {\n        ev.stopPropagation();\n        ev.preventDefault();\n        this.dragState.showDragZone = false;\n    }\n\n    async onDrop(ev) {\n        ev.preventDefault();\n        const files = ev.dataTransfer.files;\n        const resModel = this.props.list.config.resModel;\n        if (files.length) {\n            await this.env.bus.trigger(\"change_file_input\", { files, resModel });\n        }\n        this.dragState.showDragZone = false;\n    }\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, 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        if (this.root) {\n            this.waitForPagesToLoad();\n        }\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        const scale = this.getCanvasScale();\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 / scale;\n        layer.height = viewer.offsetHeight / scale;\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 handleTouchMove = (e) => {\n            if (e.touches.length == 2) {\n                // To allow zooming in pdf only.\n                e.preventDefault();\n            }\n        };\n        pinchTarget.addEventListener(\"touchmove\", handleTouchMove);\n        this.cleanupFns.push(() => {\n            pinchTarget.removeEventListener(\"touchmove\", handleTouchMove);\n        });\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        const placeholder =\n            signItem.placeholder ||\n            (isCurrentRole && signItem.name) ||\n            (this.readonly && `${signItem.name}\\n${signItem.responsible_name}`) ||\n            \"\";\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: placeholder,\n            classes: `\n                ${isCurrentRole ? \"o_sign_sign_item_default\" : \"\"}\n                ${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 && signItem.hasAttribute(\"data-signature\")) {\n                    signItem.addEventListener(\"dragstart\", (event) => {\n                        event.preventDefault();\n                    });\n                }\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(), 1000);\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    /**\n     *\n     * @returns scale of the canvas if its height exceeds the maximum size, else 1.\n     */\n    getCanvasScale() {\n        //@see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas#maximum_canvas_size\n        const MAX_CANVAS_HEIGHT = 16384;\n        const viewer_height = this.root.querySelector(\"#viewer\").offsetHeight;\n        return Math.ceil(viewer_height / MAX_CANVAS_HEIGHT);\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_validate_button\");\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            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.style.fontFamily = \"Helvetica\";\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 {\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 (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.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.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.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 { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { registry } from \"@web/core/registry\";\nimport { Component } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nconst cogMenuRegistry = registry.category(\"cogMenu\");\n\n/**\n * 'Request signature' menu\n *\n * This component is used to request a signature and link it to a document.\n * @extends Component\n */\nexport class SignRequestCogMenu extends Component {\n    static template = \"sign.SignRequestCogMenu\";\n    static components = { DropdownItem };\n    static props = {};\n\n    setup() {\n        this.action = useService(\"action\");\n    }\n\n    signRequest() {\n        /*\n         * Fetch resModel from the controller props as it is static until the view changes.\n         * Fetch resId from the current state since the state is mutable with newly created records.\n         */\n        const resModel = this.action.currentController.props.resModel;\n        const resId = this.action.currentController.currentState?.resId;\n        const referenceDoc = resId && resModel ? `${resModel},${resId}` : false;\n        if (referenceDoc) {\n            this.action.doAction(\n                {\n                    name: _t(\"Signature Request\"),\n                    type: \"ir.actions.act_window\",\n                    view_mode: \"form\",\n                    views: [[false, \"form\"]],\n                    target: \"new\",\n                    res_model: \"sign.send.request\",\n                },\n                {\n                    additionalContext: {\n                        sign_directly_without_mail: false,\n                        default_reference_doc: referenceDoc,\n                    },\n                }\n            );\n        }\n    }\n}\n\nexport const SignRequestCogMenuItem = {\n    Component: SignRequestCogMenu,\n    isDisplayed: async ({ config, searchModel }) => {\n        const is_mail_thread = searchModel.searchViewFields?.['message_ids'];\n        return (\n            searchModel.resModel !== \"sign.request\" &&\n            is_mail_thread &&\n            config.viewType === \"form\" &&\n            config.actionType === \"ir.actions.act_window\"\n        );\n    },\n    groupNumber: 1,\n};\n\ncogMenuRegistry.add(\"sign-request-menu\", SignRequestCogMenuItem, { sequence: 10 });\n", "/** @odoo-module **/\n\nimport { renderToString } from \"@web/core/utils/render\";\nimport {\n    startHelperLines,\n    offset,\n    normalizePosition,\n    generateRandomId,\n    startSmoothScroll,\n    startResize,\n} from \"@sign/components/sign_request/utils\";\nimport { InitialsAllPagesDialog } from \"@sign/dialogs/initials_all_pages_dialog\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\n\n/**\n * Mixin that adds edit features into PDF_iframe classes like drag/drop, resize, helper lines\n * Currently, it should be used only for EditWhileSigningSignablePDFIframe and SignTemplateIframe\n * Parent class should implement allowEdit and saveChanges\n *\n * @param { class } pdfClass\n * @returns class\n */\nexport const EditablePDFIframeMixin = (pdfClass) =>\n    class extends pdfClass {\n        /**\n         * Callback executed when a sign item is resized\n         * @param {SignItem} signItem\n         * @param {Object} change object with new width and height of sign item\n         * @param {Boolean} end boolean indicating if the resize is done or still in progress\n         */\n        onResizeItem(signItem, change, end = false) {\n            this.setCanvasVisibility(\"hidden\");\n            this.helperLines.show(signItem.el);\n            /**\n             * Apply the changes only if they respect the minimum width/height.\n             * The minimum width is 5.5% of the page width\n             * The minimum height is 1% of the page height\n             */\n            if (change.width >= 0.055 && change.height >= 0.01) {\n                Object.assign(signItem.el.style, {\n                    height: `${change.height * 100}%`,\n                    width: `${change.width * 100}%`,\n                });\n                Object.assign(signItem.data, {\n                    width: change.width,\n                    height: change.height,\n                    updated: true,\n                });\n                Object.assign(this.getSignItemById(signItem.data.id).data, {\n                    width: change.width,\n                    height: change.height,\n                    updated: true,\n                });\n                this.updateSignItemFontSize(signItem);\n            }\n            if (end) {\n                this.helperLines.hide();\n                this.setCanvasVisibility(\"visible\");\n                this.saveChanges();\n            }\n        }\n\n        get allowEdit() {\n            return false;\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         * Changes visibility of the canvas_layer_0 that is used for drawing connecting lines between sign items of type radio.\n         * @param {string} visibility\n         */\n        setCanvasVisibility(visibility) {\n            const canvas_layer = this.getPageContainer(1).parentElement.parentElement.querySelector(\"#canvas_layer_0\");\n            if(canvas_layer){\n                canvas_layer.style.visibility = visibility;\n            }\n        }\n\n        /**\n         * @override\n         */\n        renderSignItem() {\n            const signItem = super.renderSignItem(...arguments);\n            if (isMobileOS()) {\n                for (const node of signItem.querySelectorAll(\n                    \".o_sign_config_handle, .o_resize_handler\"\n                )) {\n                    node.classList.add(\"d-none\");\n                }\n            }\n            return signItem;\n        }\n\n        renderSignItems() {\n            super.renderSignItems();\n            if (this.allowEdit) {\n                this.startDragAndDrop();\n                this.helperLines = startHelperLines(this.root);\n            }\n        }\n\n        startDragAndDrop() {\n            this.root.querySelectorAll(\".page\").forEach((page) => {\n                page.addEventListener(\"dragover\", (e) => this.onDragOver(e));\n                page.addEventListener(\"drop\", (e) => this.onDrop(e));\n            });\n\n            this.root.querySelectorAll(\".o_sign_field_type_button\").forEach((sidebarItem) => {\n                sidebarItem.setAttribute(\"draggable\", true);\n                sidebarItem.addEventListener(\"dragstart\", (e) => this.onSidebarDragStart(e));\n                sidebarItem.addEventListener(\"dragend\", (e) => this.onSidebarDragEnd(e));\n            });\n        }\n\n        onDragStart(e) {\n            this.setCanvasVisibility(\"hidden\");\n            const signItem = e.currentTarget.parentElement.parentElement.parentElement;\n            const page = signItem.parentElement;\n            e.dataTransfer.effectAllowed = \"move\";\n            e.dataTransfer.setData(\"page\", page.dataset.pageNumber);\n            e.dataTransfer.setData(\"id\", signItem.dataset.id);\n            e.dataTransfer.setDragImage(signItem, 0, 0);\n            // workaround to hide element while keeping the drag image visible\n            requestAnimationFrame(() => {\n                if (signItem) {\n                    signItem.style.visibility = \"hidden\";\n                }\n            }, 0);\n            this.scrollCleanup = startSmoothScroll(\n                this.root.querySelector(\"#viewerContainer\"),\n                signItem,\n                null,\n                this.helperLines\n            );\n        }\n\n        onDragEnd(e) {\n            this.scrollCleanup();\n            // Make sign item visible again after dragging ends. It is a mandatory step when\n            // moving items out of bounds since in that flow the items don't get re-rendered.\n            let signItem = e.currentTarget.parentElement.parentElement.parentElement;\n            if (signItem)\n                signItem.style.visibility = \"visible\";\n            this.setCanvasVisibility(\"visible\");\n        }\n\n        onSidebarDragStart(e) {\n            this.setCanvasVisibility(\"hidden\");\n            const signTypeElement = e.currentTarget;\n            const firstPage = this.root.querySelector('.page[data-page-number=\"1\"]');\n            firstPage.insertAdjacentHTML(\n                \"beforeend\",\n                renderToString(\n                    \"sign.signItem\",\n                    this.createSignItemDataFromType(signTypeElement.dataset.itemTypeId)\n                )\n            );\n            this.ghostSignItem = firstPage.lastChild;\n            const itemData = this.signItemTypesById[signTypeElement.dataset.itemTypeId];\n            this.updateSignItemFontSize({el: this.ghostSignItem, data: {type: itemData.item_type}});\n            e.dataTransfer.setData(\"typeId\", signTypeElement.dataset.itemTypeId);\n            e.dataTransfer.setDragImage(this.ghostSignItem, 0, 0);\n            this.scrollCleanup = startSmoothScroll(\n                this.root.querySelector(\"#viewerContainer\"),\n                e.currentTarget,\n                this.ghostSignItem,\n                this.helperLines\n            );\n            // workaround to set original element to hidden while keeping the cloned element visible\n            requestAnimationFrame(() => {\n                if (this.ghostSignItem) {\n                    this.ghostSignItem.style.visibility = \"hidden\";\n                }\n            }, 0);\n        }\n\n        onSidebarDragEnd() {\n            this.scrollCleanup();\n            const firstPage = this.root.querySelector('.page[data-page-number=\"1\"]');\n            firstPage.removeChild(this.ghostSignItem);\n            this.ghostSignItem = false;\n            this.setCanvasVisibility(\"visible\");\n        }\n\n        onDragOver(e) {\n            e.preventDefault();\n            e.dataTransfer.dropEffect = \"move\";\n        }\n\n        onDrop(e) {\n            e.preventDefault();\n            const page = e.currentTarget;\n            const textLayer = page.querySelector(\".textLayer\");\n            const targetPage = Number(page.dataset.pageNumber);\n\n            const { top, left } = offset(textLayer);\n            const typeId = e.dataTransfer.getData(\"typeId\");\n            const box = textLayer.getBoundingClientRect();\n            const height = box.bottom - box.top;\n            const width = box.right - box.left;\n            if (typeId) {\n                const id = generateRandomId();\n                const data = this.createSignItemDataFromType(typeId);\n                const posX =\n                    Math.round(normalizePosition((e.pageX - left) / width, data.width) * 1000) /\n                    1000;\n                const posY =\n                    Math.round(normalizePosition((e.pageY - top) / height, data.height) * 1000) /\n                    1000;\n                Object.assign(data, { id, posX, posY, page: targetPage });\n                if (data.type === \"initial\") {\n                    this.helperLines.hide();\n                    if (this.pageCount > 1) {\n                        return this.openDialogAfterInitialDrop(data);\n                    }\n                } else if (data.type == \"radio\") {\n                    return this.addRadioSet(data);\n                }\n                this.signItems[targetPage][id] = {\n                    data,\n                    el: this.renderSignItem(data, page),\n                };\n                this.refreshSignItems();\n            } else if (e.dataTransfer.getData(\"page\") && e.dataTransfer.getData(\"id\")) {\n                const initialPage = Number(e.dataTransfer.getData(\"page\"));\n                const id = Number(e.dataTransfer.getData(\"id\"));\n                const signItem = this.signItems[initialPage][id];\n                const signItemEl = signItem.el;\n                const posX =\n                    Math.round(\n                        normalizePosition((e.pageX - left) / width, signItem.data.width) * 1000\n                    ) / 1000;\n                const posY =\n                    Math.round(\n                        normalizePosition((e.pageY - top) / height, signItem.data.height) * 1000\n                    ) / 1000;\n\n                if (initialPage !== targetPage) {\n                    signItem.data.page = targetPage;\n                    this.signItems[targetPage][id] = signItem;\n                    delete this.signItems[initialPage][id];\n                    page.appendChild(signItemEl.parentElement.removeChild(signItemEl));\n                }\n\n                Object.assign(signItem.data, {\n                    posX,\n                    posY,\n                    updated: true,\n                });\n                Object.assign(signItemEl.style, {\n                    top: `${posY * 100}%`,\n                    left: `${posX * 100}%`,\n                    visibility: \"visible\",\n                });\n                // Re-bind updated signItem to the resize function.\n                startResize(signItem, this.onResizeItem.bind(this));\n            } else {\n                return;\n            }\n\n            this.saveChanges();\n        }\n\n        /**\n         * Enables resizing and drag/drop for sign items\n         * @param {SignItem} signItem\n         */\n        enableCustom(signItem) {\n            super.enableCustom(signItem);\n            if (signItem.data.isSignItemEditable) {\n                startResize(signItem, this.onResizeItem.bind(this));\n                this.registerDragEventsForSignItem(signItem);\n            }\n        }\n\n        openDialogAfterInitialDrop(data) {\n            this.dialog.add(InitialsAllPagesDialog, {\n                addInitial: (role, targetAllPages) => {\n                    data.responsible = role;\n                    this.currentRole = role;\n                    this.addInitialSignItem(data, targetAllPages);\n                },\n                responsible: this.currentRole,\n                roles: this.signRolesById,\n                pageCount: this.pageCount\n            });\n        }\n\n        /**\n         * Inserts initial sign items in the page\n         * @param {Object} data data of the sign item to be added\n         * @param {Boolean} targetAllPages if the item should be added in all pages or only at the current one\n         */\n        addInitialSignItem(data, targetAllPages = false) {\n            if (targetAllPages) {\n                for (let page = 1; page <= this.pageCount; page++) {\n                    const hasSignatureItemsAtPage = Object.values(this.signItems[page]).some(\n                        ({ data }) => data.type === \"signature\"\n                    );\n                    if (!hasSignatureItemsAtPage) {\n                        const id = generateRandomId();\n                        const signItemData = { ...data, ...{ page, id } };\n                        this.signItems[page][id] = {\n                            data: signItemData,\n                            el: this.renderSignItem(signItemData, this.getPageContainer(page)),\n                        };\n                    }\n                }\n            } else {\n                this.signItems[data.page][data.id] = {\n                    data,\n                    el: this.renderSignItem(data, this.getPageContainer(data.page)),\n                };\n            }\n            this.saveChanges();\n        }\n\n        /**\n         * Creates and renders the inital two sign items of the radio set.\n         * @param: {Object} data: the first radio item data\n         */\n        addRadioSet(data) {\n            const id2 = generateRandomId();\n            const signItemData1 = { ...data };\n            const signItemData2 = { ...data };\n            signItemData2['id'] = id2;\n            signItemData2['posY'] += 0.025;\n            this.signItems[data.page][data.id] = {\n                data: signItemData1,\n                el: this.renderSignItem(signItemData1, this.getPageContainer(data.page)),\n            }\n            this.signItems[data.page][id2] = {\n                data: signItemData2,\n                el: this.renderSignItem(signItemData2, this.getPageContainer(data.page)),\n            }\n            this.refreshSignItems();\n            this.saveChanges();\n        }\n\n        saveChanges() {}\n\n        registerDragEventsForSignItem(signItem) {\n            const handle = signItem.el.querySelector(\".o_sign_config_handle\");\n            handle.setAttribute(\"draggable\", true);\n            handle.addEventListener(\"dragstart\", (e) => this.onDragStart(e));\n            handle.addEventListener(\"dragend\", (e) => this.onDragEnd(e));\n        }\n\n        /**\n         * Deletes a sign item from the template\n         * @param {SignItem} signItem\n         */\n        deleteSignItem(signItem) {\n            const { id, page } = signItem.data;\n            signItem.el.parentElement.removeChild(signItem.el);\n            delete this.signItems[page][id];\n            this.saveChanges();\n        }\n\n        /**\n         * Bulk delete of multiple sign items, saves the template only once.\n         * @param {SignItem []} deletedItems \n         */\n        async deleteSignItems(deletedItems) {\n            deletedItems.forEach((signItem) => {\n                this.deletedSignItemIds.push(signItem.data.id);\n                signItem.el.parentElement.removeChild(signItem.el);\n                delete this.signItems[signItem.data.page][signItem.data.id];\n                if (signItem.data.type == \"radio\") {\n                    this.radioSets[signItem.data.radio_set_id].num_options--;\n                    this.radioSets[signItem.data.radio_set_id].radio_item_ids = \n                        this.radioSets[signItem.data.radio_set_id].radio_item_ids.filter((id) => id != signItem.data.id);\n                }\n            })\n            await this.saveChanges();\n        }\n    };\n", "/** @odoo-module **/\n\nconst addNewFiles = (files) => {\n    sessionStorage.setItem(\"signMultiFileData\", JSON.stringify(files));\n    return true;\n};\n\nconst getNext = () => {\n    return (JSON.parse(sessionStorage.getItem(\"signMultiFileData\")) || []).shift();\n};\n\nconst removeFile = (id) => {\n    const files = JSON.parse(sessionStorage.getItem(\"signMultiFileData\")) || [];\n    sessionStorage.setItem(\n        \"signMultiFileData\",\n        JSON.stringify(\n            files.reduce((files, file) => {\n                if (file.template === id) {\n                    return files;\n                }\n                files.push(file);\n                return files;\n            }, [])\n        )\n    );\n};\n\nexport const multiFileUpload = {\n    addNewFiles: addNewFiles,\n    getNext: getNext,\n    removeFile: removeFile,\n};\nexport default multiFileUpload;\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { renderToString } from \"@web/core/utils/render\";\nimport { SignablePDFIframe } from \"@sign/components/sign_request/signable_PDF_iframe\";\nimport { EditablePDFIframeMixin } from \"@sign/backend_components/editable_pdf_iframe_mixin\";\n\nexport class EditWhileSigningSignablePDFIframe extends EditablePDFIframeMixin(SignablePDFIframe) {\n    toggleSidebar() {\n        const sidebar = this.root.querySelector(\".o_sign_field_type_toolbar\");\n        sidebar.classList.toggle(\"d-none\");\n    }\n\n    renderSidebar() {\n        // add field type toolbar for edit mode while signing\n        if (this.allowEdit) {\n            const sideBar = renderToString(\"sign.signItemTypesSidebar\", {\n                signItemTypes: this.props.signItemTypes.filter(\n                    (type) => type.editWhileSigningAllowed\n                ),\n                sidebarHidden: true,\n            });\n            this.root.body.insertAdjacentHTML(\"afterbegin\", sideBar);\n            this.signRolesById = {};\n            this.signRolesById[this.currentRole] = { name: this.props.currentName };\n        }\n    }\n\n    get allowEdit() {\n        return !this.readonly && this.props.templateEditable && !this.env.isSmall;\n    }\n\n    registerCloseEvent(signItem) {\n        const configArea = signItem.el.querySelector(\".o_sign_config_area\");\n        const closeButton = configArea.querySelector(\".fa-times\");\n        closeButton.addEventListener(\n            \"click\",\n            () => {\n                this.deleteSignItem(signItem);\n                this.checkSignItemsCompletion();\n            },\n            { once: true }\n        );\n    }\n\n    showBanner() {\n        super.showBanner();\n        const nameList = this.signInfo.get(\"nameList\");\n        if (nameList && nameList.length > 0) {\n            const nextName = nameList[0];\n            const bannerTitle = _t(\"Validate & the next signatory is \u201c%s\u201d\", nextName);\n            this.props.validateButton.textContent = bannerTitle;\n        }\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            isSignItemEditable: true,\n            responsible: this.currentRole,\n            option_ids: [],\n            options: [],\n            name: type.name,\n            width: type.defaultWidth,\n            height: type.defaultHeight,\n            alignment: \"center\",\n            type: type.item_type,\n            placeholder: type.placeholder,\n            classes: \"o_sign_sign_item_required\",\n            style: `width: ${type.defaultWidth * 100}%; height: ${type.defaultHeight * 100}%;`,\n            type_id: type.id,\n        };\n    }\n\n    enableCustom(signItem) {\n        super.enableCustom(signItem);\n        if (signItem.data.isSignItemEditable) {\n            this.registerCloseEvent(signItem);\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, onWillStart, useRef, markup } from \"@odoo/owl\";\nimport { SignRequestControlPanel } from \"@sign/backend_components/sign_request/sign_request_control_panel\";\nimport { Document } from \"@sign/components/sign_request/document_signable\";\nimport { PDFIframe } from \"@sign/components/sign_request/PDF_iframe\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\n\nexport class SignRequest extends Component {\n    static template = \"sign.SignRequest\";\n    static components = {\n        SignRequestControlPanel,\n        Document,\n    };\n    static props = { ...standardActionServiceProps };\n\n    get markupHtml() {\n        return markup(this.html);\n    }\n\n    get documentProps() {\n        return {\n            parent: this.documentRoot,\n            PDFIframeClass: PDFIframe,\n        };\n    }\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.notification = useService(\"notification\");\n        this.action = useService(\"action\");\n        this.signInfo = useService(\"signInfo\");\n        const action = this.props.action;\n        const context = action?.context;\n\n        this.signInfo.reset({\n            documentId: context.id || action.params.id,\n            signRequestToken: context.token || action.params.token, // token could be sign.request.item's token if signabledocument\n            createUid: context.create_uid || action.params.create_uid,\n            signRequestState: context.state || action.params.state,\n            requestItemStates: context.request_item_states,\n            needToSign: context.need_to_sign,\n            todayFormattedDate: context.today_formatted_date,\n        });\n\n        if (this.signInfo) {\n            this.props.updateActionState({\n                id: this.signInfo.get(\"documentId\"),\n                token: this.signInfo.get(\"signRequestToken\"),\n                create_uid: this.signInfo.get(\"createUid\"),\n                state: this.signInfo.get(\"signRequestState\"),\n            });\n        }\n\n        this.documentRoot = useRef(\"sign-document\");\n        onWillStart(() => this.fetchDocument());\n    }\n\n    async fetchDocument() {\n        if (!this.signInfo.get(\"documentId\")) {\n            return this.goBackToKanban();\n        }\n        const { html, context } = await rpc(\n            `/sign/get_document/${this.signInfo.get(\"documentId\")}/${this.signInfo.get(\n                \"signRequestToken\"\n            )}`\n        );\n        this.html = html.trim();\n        if (Object.keys(context).length > 0) {\n            this.signInfo.set({\n                refusalAllowed: context.refusal_allowed,\n                signRequestItemToken: this.signInfo.get(\"signRequestToken\"),\n                signRequestToken: context.sign_request_token,\n            });\n        }\n\n        const parser = new DOMParser();\n        const doc = parser.parseFromString(this.html, \"text/html\");\n        this.signerStatus = doc.querySelector(\".o_sign_cp_pager\");\n    }\n\n    goBackToKanban() {\n        return this.action.doAction(\"sign.sign_request_action\", { clearBreadcrumbs: true });\n    }\n\n    get controlPanelProps() {\n        return {\n            signerStatus: this.signerStatus,\n            goBackToKanban: this.goBackToKanban.bind(this),\n        };\n    }\n}\n\nregistry.category(\"actions\").add(\"sign.Document\", SignRequest);\n", "/** @odoo-module **/\n\nimport { user } from \"@web/core/user\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Component, useEffect, useComponent, markup } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { ControlPanel } from \"@web/search/control_panel/control_panel\";\nimport multiFileUpload from \"@sign/backend_components/multi_file_upload\";\n\nfunction useResendButtons() {\n    const component = useComponent();\n    const onClickResend = async (e) => {\n        const parent = e.currentTarget.parentNode;\n        const signRequestItemId = parseInt(parent.dataset.id);\n        await component.orm.call(\n            \"sign.request.item\",\n            \"send_signature_accesses\",\n            [signRequestItemId],\n            { context: user.context }\n        );\n        e.target.innerText = _t(\"Resent!\");\n    };\n    useEffect(\n        (showResendButtons) => {\n            if (!showResendButtons) {\n                return;\n            }\n            const status = document.querySelector(\"div.signer-status\");\n            const signerNames = status.querySelectorAll(\n                \".o_sign_signer_status.o_sign_signer_waiting\"\n            );\n            Array.from(signerNames).forEach((signerNameEl) => {\n                const stateSet =\n                    component.signInfo.get(\"requestItemStates\")[signerNameEl.dataset.id];\n                const title = stateSet ? _t(\"Resend the invitation\") : _t(\"Send the invitation\");\n                const text = stateSet ? _t(\"Resend\") : _t(\"Send\");\n                const button = document.createElement(\"button\");\n                button.title = title;\n                button.innerText = text;\n                button.className = \"o_sign_resend_access_button btn btn-link ms-2 me-2\";\n                button.style = \"vertical-align: baseline;\";\n                signerNameEl.insertBefore(button, signerNameEl.firstChild);\n                button.addEventListener(\"click\", onClickResend);\n            });\n        },\n        () => [component.showResendButtons]\n    );\n}\n\nexport class SignRequestControlPanel extends Component {\n    static template = \"sign.SignRequestControlPanel\";\n    static components = {\n        ControlPanel,\n    };\n    static props = {\n        signerStatus: {\n            type: Object,\n            optional: true,\n        },\n        goBackToKanban: { type: Function },\n    };\n\n    setup() {\n        this.controlPanelDisplay = {};\n        this.action = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.signInfo = useService(\"signInfo\");\n        this.nextTemplate = multiFileUpload.getNext();\n        useResendButtons();\n    }\n\n    get markupSignerStatus() {\n        return markup(this.props.signerStatus.innerHTML);\n    }\n\n    get showResendButtons() {\n        const documentSent = this.signInfo.get(\"signRequestState\") === \"sent\";\n        const isAuthor = this.signInfo.get(\"createUid\") === user.userId;\n        return isAuthor && documentSent;\n    }\n\n    get allowCancel() {\n        const needToSign = this.signInfo.get(\"needToSign\");\n        const state = this.signInfo.get(\"signRequestState\");\n        return needToSign && ![\"signed\", \"canceled\"].includes(state);\n    }\n\n    async signDocument() {\n        const action = await this.orm.call(\"sign.request\", \"go_to_signable_document\", [\n            [this.signInfo.get(\"documentId\")],\n        ]);\n        action.name = _t(\"Sign\");\n        this.action.doAction(action);\n    }\n\n    async cancelDocument() {\n        await this.orm.call(\"sign.request\", \"cancel\", [this.signInfo.get(\"documentId\")]);\n        const result = await this.orm.call(\n            \"sign.request\",\n            \"get_close_values\",\n            [[this.signInfo.get(\"documentId\")]],\n        );\n        const context = result.custom_action ? {} : {clearBreadcrumbs: true};\n        this.env.services.action.doAction(result.action, context);\n    }\n\n    async goToNextDocument() {\n        const templateName = this.nextTemplate.name;\n        const templateId = parseInt(this.nextTemplate.template);\n        multiFileUpload.removeFile(this.nextTemplate.template);\n        await this.action.doAction(\n            {\n                type: \"ir.actions.client\",\n                tag: \"sign.Template\",\n                name: _t(\"Template %s\", templateName),\n                params: {\n                    sign_edit_call: \"sign_send_request\",\n                    id: templateId,\n                    sign_directly_without_mail: false,\n                },\n            },\n            { clear_breadcrumbs: true }\n        );\n    }\n}\n", "/** @odoo-module **/\n\nimport { useBus } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\nimport { Document } from \"@sign/components/sign_request/document_signable\";\nimport { SignRequest } from \"@sign/backend_components/sign_request/sign_request_action\";\nimport { useSubEnv, EventBus } from \"@odoo/owl\";\nimport { SignableRequestControlPanel } from \"@sign/backend_components/sign_request/signable_sign_request_control_panel\";\nimport { EditWhileSigningSignablePDFIframe } from \"@sign/backend_components/sign_request/edit_while_signing_signable_pdf_iframe\";\n\nclass EditWhileSigningDocument extends Document {\n    setup() {\n        super.setup();\n        useBus(this.env.editWhileSigningBus, \"toggleEditBar\", () => {\n            this.iframe.toggleSidebar();\n        });\n    }\n}\n\nexport class SignableSignRequest extends SignRequest {\n    static components = {\n        ...SignableSignRequest.components,\n        Document: EditWhileSigningDocument,\n        SignRequestControlPanel: SignableRequestControlPanel,\n    };\n    setup() {\n        super.setup();\n        this.signInfo.set({\n            editWhileSigning: this.props.action.context.template_editable,\n            tokenList: this.tokenList,\n            nameList: this.nameList,\n        });\n        useSubEnv({\n            editWhileSigningBus: new EventBus(),\n        });\n    }\n\n    get nameList() {\n        return this.props.action.context.name_list;\n    }\n\n    get tokenList() {\n        return this.props.action.context.token_list;\n    }\n\n    get documentProps() {\n        return {\n            ...super.documentProps,\n            PDFIframeClass: EditWhileSigningSignablePDFIframe,\n        };\n    }\n}\n\nregistry.category(\"actions\").add(\"sign.SignableDocument\", SignableSignRequest);\n", "/** @odoo-module **/\n\nimport { ControlPanel } from \"@web/search/control_panel/control_panel\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, markup } from \"@odoo/owl\";\nimport { SignRefusalDialog } from \"@sign/dialogs/dialogs\";\n\nexport class SignableRequestControlPanel extends Component {\n    static template = \"sign.SignSignableRequestControlPanel\";\n    static components = {\n        ControlPanel,\n    };\n    static props = {\n        signerStatus: { type: \"String\" },\n        goBackToKanban: { type: Function },\n    };\n\n    setup() {\n        this.controlPanelDisplay = {};\n        this.action = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.dialog = useService(\"dialog\");\n        this.signInfo = useService(\"signInfo\");\n    }\n\n    refuseDocument() {\n        this.dialog.add(SignRefusalDialog);\n    }\n\n    get markupSignerStatus() {\n        return markup(this.props.signerStatus.innerHTML);\n    }\n\n    toggleEditBar() {\n        this.env.editWhileSigningBus.trigger(\"toggleEditBar\");\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Record } from \"@web/model/record\";\nimport {\n    many2ManyTagsField,\n    Many2ManyTagsField,\n} from \"@web/views/fields/many2many_tags/many2many_tags_field\";\nimport { Many2OneField } from \"@web/views/fields/many2one/many2one_field\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nconst actionFieldsGet = {\n    option_ids: { type: \"many2many\", relation: \"sign.item.option\", string: \"Selected Options\" },\n    responsible_id: { type: \"many2one\", relation: \"sign.item.role\", string: \"Responsible\" },\n};\n\nfunction getActionActiveFields() {\n    const activeFields = {};\n    for (const fName of Object.keys(actionFieldsGet)) {\n        if (actionFieldsGet[fName].type === \"many2many\") {\n            const related = Object.fromEntries(\n                many2ManyTagsField.relatedFields({ options: {} }).map((f) => [f.name, f])\n            );\n            activeFields[fName] = {\n                related: {\n                    activeFields: related,\n                    fields: related,\n                },\n            };\n        } else {\n            activeFields[fName] = actionFieldsGet[fName];\n        }\n    }\n    return activeFields;\n}\n\nexport class SignItemCustomPopover extends Component {\n    static template = \"sign.SignItemCustomPopover\";\n    static components = {\n        Record,\n        Many2ManyTagsField,\n        Many2OneField,\n    };\n    static props = {\n        id: { type: Number },\n        alignment: { type: String },\n        header_title: {type: String },\n        placeholder: { type: String },\n        required: { type: Boolean },\n        option_ids: { type: Array },\n        responsible: { type: Number },\n        onValidate: { type: Function },\n        updateSelectionOptions: { type: Function },\n        updateRoles: { type: Function },\n        type: { type: String },\n        onDelete: { type: Function },\n        onClose: { type: Function },\n        debug: { type: String },\n        roles: { type: Object },\n        close: { type: Function },\n        num_options: {type: Number, optional: true},\n        radio_set_id: {type: Number, optional: true},\n    };\n\n    setup() {\n        this.alignmentOptions = [\n            { title: _t(\"Left\"), key: \"left\" },\n            { title: _t(\"Center\"), key: \"center\" },\n            { title: _t(\"Right\"), key: \"right\" },\n        ];\n        this.state = useState({\n            alignment: this.props.alignment,\n            placeholder: this.props.placeholder,\n            required: this.props.required,\n            option_ids: this.props.option_ids,\n            responsible: this.props.responsible,\n            num_options: this.props.num_options,\n            radio_set_id: this.props.radio_set_id,\n        });\n        this.signItemFieldsGet = getActionActiveFields();\n        this.typesWithAlignment = new Set([\"text\", \"textarea\"]);\n    }\n\n    handleNumOptionsChange(value) {\n        if (Number(value) < 2) {\n            return;\n        }\n        this.state['num_options'] = Number(value);\n    }\n\n    onChange(key, value) {\n        this.state[key] = value;\n    }\n\n    onValidate() {\n        this.props.onValidate(this.state);\n    }\n\n    get recordProps() {\n        return {\n            mode: \"edit\",\n            resModel: \"sign.item\",\n            resId: this.props.id,\n            fieldNames: this.signItemFieldsGet,\n            activeFields: this.signItemFieldsGet,\n            onRecordChanged: async (record, changes) => {\n                if (changes.option_ids) {\n                    const ids = record.data.option_ids.currentIds;\n                    this.state.option_ids = ids;\n                    this.props.updateSelectionOptions(ids);\n                }\n                if (changes.responsible_id) {\n                    const id = changes.responsible_id;\n                    this.state.responsible = id;\n                    this.props.updateRoles(id);\n                }\n            },\n        };\n    }\n\n    getMany2XProps(record, fieldName) {\n        return {\n            name: fieldName,\n            id: fieldName,\n            record,\n            readonly: false,\n            canCreateEdit: false,\n            canQuickCreate: true,\n        };\n    }\n\n    getOptionsProps(record, fieldName) {\n        return {\n            ...this.getMany2XProps(record, fieldName),\n            domain: [[\"available\", \"=\", true]],\n            noSearchMore: true,\n        };\n    }\n\n    get showAlignment() {\n        return this.typesWithAlignment.has(this.props.type);\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { SignTemplateControlPanel } from \"./sign_template_control_panel\";\nimport { SignTemplateBody } from \"./sign_template_body\";\nimport { Component, onWillStart } from \"@odoo/owl\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\n\nexport class SignTemplate extends Component {\n    static template = \"sign.Template\";\n    static components = {\n        SignTemplateControlPanel,\n        SignTemplateBody,\n    };\n    static props = {\n        ...standardActionServiceProps,\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.notification = useService(\"notification\");\n        this.action = useService(\"action\");\n        const params = this.props.action.params;\n        this.templateID = params.id;\n        if (this.templateID) {\n            this.props.updateActionState({ id: this.templateID });\n        }\n        this.actionType = params.sign_edit_call || \"\";\n        this.resModel = params.resModel || \"\";\n        onWillStart(async () => {\n            if (!this.templateID) {\n                return this.goBackToKanban();\n            }\n            return Promise.all([this.checkManageTemplateAccess(), this.fetchTemplateData()]);\n        });\n    }\n\n    async fetchTemplateData() {\n        const template = await this.orm.call(\"sign.template\", \"read\", [\n            [this.templateID],\n            [\"id\", \"attachment_id\", \"has_sign_requests\", \"responsible_count\", \"display_name\", \"active\"],\n        ]);\n        if (!template.length) {\n            this.templateID = undefined;\n            this.notification.add(_t(\"The template doesn't exist anymore.\"), {\n                title: _t(\"Warning\"),\n                type: \"warning\",\n            });\n            return;\n        }\n        this.signTemplate = template[0];\n        this.hasSignRequests = this.signTemplate.has_sign_requests;\n        this.responsibleCount = this.signTemplate.responsible_count;\n        this.attachmentLocation = `/web/content/${this.signTemplate.attachment_id[0]}`;\n\n        return Promise.all([\n            this.fetchSignItemData(),\n            this.fetchAttachment(),\n            this.fetchSignItemTypes(),\n            this.fetchSignRoles(),\n            this.fetchRadioSets(),\n        ]);\n    }\n\n    async fetchRadioSets() {\n        this.radioSets = await this.orm.call(\n            \"sign.template\",\n            \"get_radio_sets_dict\", [\n                this.signTemplate.id\n            ]\n        );\n    }\n\n    async fetchSignItemTypes() {\n        this.signItemTypes = await this.orm.call(\"sign.item.type\", \"search_read\", [], {\n            context: user.context,\n        });\n    }\n\n    async fetchSignRoles() {\n        this.signRoles = await this.orm.call(\"sign.item.role\", \"search_read\", [], {\n            context: user.context,\n        });\n    }\n\n    async fetchSignItemData() {\n        this.signItems = await this.orm.call(\n            \"sign.item\",\n            \"search_read\",\n            [[[\"template_id\", \"=\", this.signTemplate.id]]],\n            { context: user.context }\n        );\n\n        // The ORM would format radio_set_id like: [49, 'sign.item.radio.set,49']\n        // The format isn't important, we care only about the id.\n        this.signItems.forEach((item) => {\n            item.radio_set_id = item?.radio_set_id[0] || undefined;\n        });\n\n        this.signItemOptions = await this.orm.call(\n            \"sign.item.option\",\n            \"search_read\",\n            [[], [\"id\", \"value\"]],\n            { context: user.context }\n        );\n    }\n\n    async fetchAttachment() {\n        const attachment = await this.orm.call(\n            \"ir.attachment\",\n            \"read\",\n            [[this.signTemplate.attachment_id[0]], [\"mimetype\", \"name\"]],\n            { context: user.context }\n        );\n\n        this.signTemplateAttachment = attachment[0];\n        this.isPDF = this.signTemplateAttachment.mimetype.indexOf(\"pdf\") > -1;\n    }\n\n    /**\n     * Checks that user has group sign.manage_template_access for showing extra fields\n     */\n    async checkManageTemplateAccess() {\n        this.manageTemplateAccess = await user.hasGroup(\"sign.manage_template_access\");\n    }\n\n    goBackToKanban() {\n        return this.action.doAction(\"sign.sign_template_action\", { clearBreadcrumbs: true });\n    }\n}\n\nregistry.category(\"actions\").add(\"sign.Template\", SignTemplate);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { SignTemplateIframe } from \"./sign_template_iframe\";\nimport { SignTemplateTopBar } from \"./sign_template_top_bar\";\nimport { Component, useRef, useEffect, onWillUnmount } from \"@odoo/owl\";\nimport { buildPDFViewerURL } from \"@sign/components/sign_request/utils\";\nimport { AlertDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\n\nexport class SignTemplateBody extends Component {\n    static template = \"sign.SignTemplateBody\";\n    static components = {\n        SignTemplateTopBar,\n    };\n    static props = {\n        signItemTypes: { type: Array },\n        signItems: { type: Array },\n        signRoles: { type: Array },\n        radioSets: { type: Object },\n        hasSignRequests: { type: Boolean },\n        signItemOptions: { type: Array },\n        attachmentLocation: { type: String },\n        signTemplate: { type: Object },\n        goBackToKanban: { type: Function },\n        manageTemplateAccess: { type: Boolean },\n        isPDF: { type: Boolean },\n        resModel: { type: String },\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.notification = useService(\"notification\");\n        this.popover = useService(\"popover\");\n        this.dialog = useService(\"dialog\");\n        this.PDFIframe = useRef(\"PDFIframe\");\n        this.PDFViewerURL = buildPDFViewerURL(this.props.attachmentLocation, this.env.isSmall);\n        useEffect(\n            () => {\n                return this.waitForPDF();\n            },\n            () => []\n        );\n        onWillUnmount(() => {\n            if (this.iframe) {\n                this.saveTemplate();\n                this.iframe.unmount();\n                this.iframe = null;\n                if (this.props.signTemplate.active) {\n                    this.notification.add(_t(\"Saved\"), {type: \"success\"});\n                }\n            }\n        });\n    }\n\n    waitForPDF() {\n        this.PDFIframe.el.onload = () => setTimeout(() => this.doPDFPostLoad(), 1);\n    }\n\n    doPDFPostLoad() {\n        this.preventDroppingImagesOnViewerContainer();\n        this.iframe = new SignTemplateIframe(\n            this.PDFIframe.el.contentDocument,\n            this.env,\n            {\n                orm: this.orm,\n                popover: this.popover,\n                dialog: this.dialog,\n            },\n            {\n                signItemTypes: this.props.signItemTypes,\n                signItems: this.props.signItems,\n                signRoles: this.props.signRoles,\n                hasSignRequests: this.props.hasSignRequests,\n                signItemOptions: this.props.signItemOptions,\n                radioSets: this.props.radioSets,\n                saveTemplate: () => this.saveTemplate(),\n                getRadioSetInfo: (id) => this.getRadioSetInfo(id),\n                rotatePDF: () => this.rotatePDF(),\n            }\n        );\n    }\n\n    /**\n     * Prevents opening files in the pdf js viewer when dropping files/images to the viewerContainer\n     * Ref: https://stackoverflow.com/a/68939139\n     */\n    preventDroppingImagesOnViewerContainer() {\n        const viewerContainer = this.PDFIframe.el.contentDocument.querySelector(\"#viewerContainer\");\n        viewerContainer.addEventListener(\n            \"drop\",\n            (e) => {\n                if (e.dataTransfer.files && e.dataTransfer.files.length) {\n                    e.stopImmediatePropagation();\n                    e.stopPropagation();\n                }\n            },\n            true\n        );\n    }\n\n    onTemplateNameChange(e) {\n        const value = e.target.value;\n        if (value != \"\") {\n            this.props.signTemplate.display_name = value;\n            this.saveTemplate(value);\n        }\n    }\n\n    async saveTemplate(newTemplateName) {\n        const [updatedSignItems, Id2UpdatedItem] = this.prepareTemplateData();\n        const newId2ItemIdMap = await this.orm.call(\"sign.template\", \"update_from_pdfviewer\", [\n            this.props.signTemplate.id,\n            updatedSignItems,\n            this.iframe.deletedSignItemIds,\n            newTemplateName || \"\",\n        ]);\n\n        if (!newId2ItemIdMap) {\n            this.showBlockedTemplateDialog();\n            return false;\n        }\n\n        for (const [newId, itemId] of Object.entries(newId2ItemIdMap)) {\n            Id2UpdatedItem[newId].id = itemId;\n        }\n        return Id2UpdatedItem;\n    }\n\n    async getRadioSetInfo(sign_item_ids) {\n        const info = await this.orm.call(\"sign.template\", \"get_radio_set_info_by_item_id\", [\n            this.props.signTemplate.id,\n            sign_item_ids,\n        ])\n        return info;\n    }\n\n    prepareTemplateData() {\n        const updatedSignItems = {};\n        const Id2UpdatedItem = {};\n        const items = this.iframe?.signItems ?? {};\n        for (const page in items) {\n            for (const id in items[page]) {\n                const signItem = items[page][id].data;\n                if (signItem.updated) {\n                    Id2UpdatedItem[id] = signItem;\n                    const responsible = signItem.responsible;\n                    updatedSignItems[id] = {\n                        type_id: signItem.type_id[0],\n                        required: signItem.required,\n                        name: signItem.placeholder || signItem.name,\n                        alignment: signItem.alignment,\n                        option_ids: signItem.option_ids,\n                        responsible_id: responsible,\n                        page: page,\n                        posX: signItem.posX,\n                        posY: signItem.posY,\n                        width: signItem.width,\n                        height: signItem.height,\n                        radio_set_id: signItem.radio_set_id,\n                    };\n\n                    if (id < 0) {\n                        updatedSignItems[id][\"transaction_id\"] = id;\n                    }\n                }\n            }\n        }\n        return [updatedSignItems, Id2UpdatedItem];\n    }\n\n    async rotatePDF() {\n        const result = await this.orm.call(\"sign.template\", \"rotate_pdf\", [\n            this.props.signTemplate.id,\n        ]);\n        if (!result) {\n            this.showBlockedTemplateDialog();\n        }\n\n        return result;\n    }\n\n    showBlockedTemplateDialog() {\n        this.dialog.add(AlertDialog, {\n            confirm: () => {\n                this.props.goBackToKanban();\n            },\n            body: _t(\"Somebody is already filling a document which uses this template\"),\n        });\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component } from \"@odoo/owl\";\nimport { ControlPanel } from \"@web/search/control_panel/control_panel\";\nimport { multiFileUpload } from \"@sign/backend_components/multi_file_upload\";\n\nexport class SignTemplateControlPanel extends Component {\n    static template = \"sign.SignTemplateControlPanel\";\n    static components = {\n        ControlPanel,\n    };\n    static props = {\n        responsibleCount: { type: Number },\n        isPDF: { type: Boolean },\n        hasSignRequests: { type: Boolean },\n        actionType: { type: String },\n        signTemplate: { type: Object },\n        goBackToKanban: { type: Function },\n    };\n\n    setup() {\n        this.controlPanelDisplay = {};\n        this.nextTemplate = multiFileUpload.getNext() ?? false;\n        this.action = useService(\"action\");\n        this.orm = useService(\"orm\");\n    }\n\n    get showShareButton() {\n        return this.props.actionType !== \"sign_send_request\" && this.props.responsibleCount <= 1;\n    }\n\n    onSendClick() {\n        this.action.doAction(\"sign.action_sign_send_request\", {\n            additionalContext: {\n                active_id: this.props.signTemplate.id,\n                sign_directly_without_mail: false,\n                show_email: true,\n            },\n        });\n    }\n\n    onSignNowClick() {\n        this.action.doAction(\"sign.action_sign_send_request\", {\n            additionalContext: {\n                active_id: this.props.signTemplate.id,\n                sign_directly_without_mail: true,\n            },\n        });\n    }\n\n    async onShareClick() {\n        const action = await this.orm.call(\"sign.template\", \"open_shared_sign_request\", [\n            this.props.signTemplate.id,\n        ]);\n        this.action.doAction(action);\n    }\n\n    onNextDocumentClick() {\n        const templateName = this.nextTemplate.name;\n        const templateId = parseInt(this.nextTemplate.template);\n        multiFileUpload.removeFile(templateId);\n        this.action.doAction({\n            type: \"ir.actions.client\",\n            tag: \"sign.Template\",\n            name: _t(\"Template %s\", templateName),\n            params: {\n                sign_edit_call: \"sign_template_edit\",\n                id: templateId,\n                sign_directly_without_mail: false,\n            },\n        });\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { renderToString } from \"@web/core/utils/render\";\nimport { shallowEqual } from \"@web/core/utils/arrays\";\nimport { normalizePosition, startResize, generateRandomId } from \"@sign/components/sign_request/utils\";\nimport { SignItemCustomPopover } from \"@sign/backend_components/sign_template/sign_item_custom_popover\";\nimport { PDFIframe } from \"@sign/components/sign_request/PDF_iframe\";\nimport { EditablePDFIframeMixin } from \"@sign/backend_components/editable_pdf_iframe_mixin\";\nimport { user } from \"@web/core/user\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\n\nexport class SignTemplateIframe extends EditablePDFIframeMixin(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        super(root, env, owlServices, props);\n        this.deletedSignItemIds = [];\n        this.currentRole = this.props.signRoles[0].id;\n        this.closePopoverFns = {};\n        this.signItemTypesById = this.props.signItemTypes.reduce((obj, type) => {\n            obj[type.id] = type;\n            return obj;\n        }, {});\n        this.signRolesById = this.props.signRoles.reduce((obj, role) => {\n            obj[role.id] = role;\n            return obj;\n        }, {});\n        this.selectionOptionsById = this.props.signItemOptions.reduce((obj, option) => {\n            obj[option.id] = option;\n            return obj;\n        }, {});\n        /**\n         * This is used to keep track of the sign items that are currently being\n         * fetched from the server. This is used to ensure that the sign item\n         * on which a click event is triggered is completely loaded before\n         */\n        this.negativeIds = {};\n        this.radioSets = this.props.radioSets;\n    }\n\n    get allowEdit() {\n        return !this.props.hasSignRequests;\n    }\n\n    renderSidebar() {\n        super.renderSidebar();\n        if (this.allowEdit && !isMobileOS()) {\n            const sideBar = renderToString(\"sign.signItemTypesSidebar\", {\n                signItemTypes: this.props.signItemTypes,\n            });\n            this.root.body.insertAdjacentHTML(\"afterbegin\", sideBar);\n        }\n    }\n\n    /**\n     * Draws the connecting lines between two sign items. Uses `canvas_layer_0` for rendering.\n     * @param {SignItem} signItem1 \n     * @param {SignItem} signItem2 \n     */\n    renderConnectingLine(signItem1, signItem2){\n        /**\n         * For each sign item we have its coordinates inside the corresponding page.\n         * In order to allow for lines that cover multiple pages we calculate the (x, y) coordinates \n         * with respect to the entire document.\n         * \n            -----------------------------------------\n            |           |                           |\n            |   --------|----------------------     |\n            |   |       y                     |     |\n            |   |       |                     |     |\n            |____x______-----                 |     |\n            |   |       |   |                 |     |\n            |   |       -----                 |     |\n            |   |         |                   |     |\n            |   |       -----                 |     |\n            |   |       |   |                 |     |\n            |   |       -----                 |     |\n            |   |         |                   |     |\n            |   |         |         -----     |     |\n            |   |          ---------|   |     |     |\n            |   |                   -----     |     |\n            |   |                             |     |\n            |   -------------------------------     |\n            |                                       |\n            -----------------------------------------\n\n            To connect two items we do the following:\n                1. Check if we can connect them with one line (horizontal/verical)\n                2. If not, this means that the second item lies in of four quarters with respect to the first item.\n                   Check in which quarter does it lie and draw the two lines accordingly.\n\n                            |               |\n                      (1)   |               |   (2)\n                ------------|----------------------------\n                            |               |\n                            |  first item   |\n                            |               |\n                -----------------------------------------\n                            |               |\n                      (3)   |               |     (4)\n                            |               |\n         */\n\n        const page1 = this.getPageContainer(signItem1.data.page);\n        const page2 = this.getPageContainer(signItem2.data.page);\n        const dx1 = page1.offsetLeft + page1.clientLeft;\n        const dy1 = page1.offsetTop + page1.clientTop;\n        const dx2 = page2.offsetLeft + page2.clientLeft;\n        const dy2 = page2.offsetTop + page2.clientTop;\n        const x1 = signItem1.data.posX * page1.clientWidth + dx1;\n        const y1 = signItem1.data.posY * page1.clientHeight + dy1;\n        const w1 = signItem1.data.width * page1.clientWidth;\n        const h1 = signItem1.data.height * page1.clientHeight;\n        const c1 = {x : x1 + w1 / 2, y: y1 + h1 / 2};\n        const x2 = signItem2.data.posX * page2.clientWidth + dx2;\n        const y2 = signItem2.data.posY * page2.clientHeight + dy2;\n        const w2 = signItem2.data.width * page2.clientWidth;\n        const h2 = signItem2.data.height * page2.clientHeight;\n        const c2 = {x: x2 + w2 / 2, y: y2 + h2 / 2};\n        \n        if (!(x2 > x1 + w1 || x1 > x2 + w2)) {\n            //One vertical line\n            let midx = (Math.max(x1, x2) + Math.min(x1 + w1, x2 + w2)) / 2;\n            if(y1 > y2){\n                this.renderLine({\n                    start: {\n                        x: midx,\n                        y: y2 + h2,\n                    },\n                    end: {\n                        x: midx,\n                        y: y1,\n                    }\n                });\n            } else {\n                this.renderLine({\n                    start: {\n                        x: midx,\n                        y: y1 + h1,\n                    },\n                    end: {\n                        x: midx,\n                        y: y2,\n                    }\n                });\n            }\n        }\n        else if (!(y2 > y1 + h1 || y1 > y2 + h2)) {\n            //One horizontal line\n            let midy = (Math.max(y1, y2) + Math.min(y1 + h1, y2 + h2)) / 2;\n            if(x1 > x2){\n                this.renderLine({\n                    start: {\n                        x: x2 + w2,\n                        y: midy,\n                    },\n                    end: {\n                        x: x1,\n                        y: midy,\n                    }\n                });\n            } else {\n                this.renderLine({\n                    start: {\n                        x: x1 + w1,\n                        y: midy,\n                    },\n                    end: {\n                        x: x2,\n                        y: midy,\n                    }\n                });\n            }\n        } else {\n            //we need two lines to connect them\n            if (y2 + h2 < y1 && x2 + w2 < x1) {\n                //Quarter (1)\n                const corner = {\n                    x: c2.x,\n                    y: c1.y,\n                }\n                this.renderLine({\n                    start: {\n                        x: x1,\n                        y: c1.y,\n                    },\n                    end: corner\n                });\n                this.renderLine({\n                    start: corner,\n                    end:{\n                        x: c2.x,\n                        y: y2 + h2,\n                    }\n                });\n            } else if (y2 + h2 < y1 && x2 > x1 + w1) {\n                //Quarter (2)\n                const corner = {\n                    x: c2.x,\n                    y: c1.y\n                }\n                this.renderLine({\n                    start: corner,\n                    end:{\n                        x: x1 + w1,\n                        y: c1.y\n                    }\n                });\n                this.renderLine({\n                    start: corner,\n                    end:{\n                        x: c2.x,\n                        y: y2 + h2,\n                    }\n                });\n            } else if (y2 > y1 + h1 && x2 + w2 < x1) {\n                //Quarter (3)\n                const corner = {\n                    x: c1.x,\n                    y: c2.y\n                }\n                this.renderLine({\n                    start: corner,\n                    end: {\n                        x: c1.x,\n                        y: y1 + h1,\n                    }\n                });\n                this.renderLine({\n                    start: corner,\n                    end: {\n                        x: x2 + w2,\n                        y: c2.y\n                    }\n                });\n            } else if (y2 > y1 + h1 && x2 > x1 + w1) {\n                //Quarter (4)\n                const corner = {\n                    x: c1.x,\n                    y: c2.y\n                }\n                this.renderLine({\n                    start: corner,\n                    end: {\n                        x: c1.x,\n                        y: y1 + h1,\n                    }\n                });\n                this.renderLine({\n                    start: corner,\n                    end: {\n                        x: x2,\n                        y: c2.y\n                    }\n                });\n            }\n        }\n    }\n\n    /**\n     * Draws a dashed line that goes from (data.start.x, data.start.y) to (data.end.x, data.end.y)\n     * @param {Object} data\n     */\n    renderLine(data){\n        const canvas = this.getCanvas();\n        const ctx = canvas.getContext('2d');\n        const scale = this.getCanvasScale();\n        ctx.lineWidth = 3 / scale;\n        ctx.setLineDash([15 / scale, 7 / scale]);\n        ctx.strokeStyle = \"orange\";\n        ctx.beginPath();\n        ctx.moveTo(data.start.x / scale, data.start.y / scale);\n        ctx.lineTo(data.end.x / scale, data.end.y / scale);\n        ctx.stroke();\n    }\n\n    /**\n     * Renders connecting lines between sign items of type \"radio\"\n     */\n    renderAllConnectingLines() {\n        const canvas = this.getCanvas();\n        if (canvas) {\n            const ctx = canvas.getContext('2d');\n            ctx.clearRect(0, 0, canvas.width, canvas.height);\n        }\n\n        for (const radio_set_id in this.radioSets) {\n            this.radioSets[radio_set_id].radio_item_ids = this.radioSets[radio_set_id].radio_item_ids.sort((id1, id2) => {\n                const data1 = this.getSignItemById(id1).data;\n                const data2 = this.getSignItemById(id2).data;\n                return (\n                    100 * (data1.page - data2.page) +\n                    10 * (data1.posY - data2.posY) +\n                    (data1.posX - data2.posX)\n                );\n            });\n            for (let i = 1; i < this.radioSets[radio_set_id].radio_item_ids.length; i++) {\n                const item1 = this.getSignItemById(this.radioSets[radio_set_id].radio_item_ids[i - 1]);\n                const item2 = this.getSignItemById(this.radioSets[radio_set_id].radio_item_ids[i]);\n                this.renderConnectingLine(item1, item2);\n            }\n        }\n    }\n\n    registerDragEventsForSignItem(signItem) {\n        super.registerDragEventsForSignItem(signItem);\n        const display = signItem.el.querySelector(\".o_sign_item_display\");\n        display.addEventListener(\"click\", () => this.openSignItemPopup(signItem));\n    }\n\n    /**\n     * Handles opening and closing of popovers in template edition\n     * @param {SignItem} signItem\n     */\n    async openSignItemPopup(signItem) {\n        const shouldOpenNewPopover = !(signItem.data.id in this.closePopoverFns);\n        this.closePopover();\n        if (shouldOpenNewPopover) {\n            if (signItem.data.id in this.negativeIds) {\n                await this.negativeIds[signItem.data.id];\n            }\n            const header_title = signItem.data.type == \"radio\" ? \"Radio Button\" : signItem.data.placeholder;\n            const closeFn = this.popover.add(\n                signItem.el,\n                SignItemCustomPopover,\n                {\n                    debug: this.env.debug,\n                    responsible: signItem.data.responsible,\n                    roles: this.signRolesById,\n                    alignment: signItem.data.alignment,\n                    required: signItem.data.required,\n                    header_title: header_title,\n                    placeholder: signItem.data.placeholder,\n                    id: signItem.data.id,\n                    type: signItem.data.type,\n                    option_ids: signItem.data.option_ids,\n                    num_options: this.getSignItemById(signItem.data.id).data.num_options,\n                    radio_set_id: signItem.data.radio_set_id,\n                    onValidate: (data) => {\n                        this.updateSignItem(signItem, data);\n                        this.closePopover();\n                    },\n                    onDelete: () => {\n                        this.closePopover();\n                        this.deleteSignItem(signItem);\n                    },\n                    onClose: () => {\n                        this.closePopover();\n                    },\n                    updateSelectionOptions: (ids) => this.updateSelectionOptions(ids),\n                    updateRoles: (id) => this.updateRoles(id),\n                },\n                {\n                    position: \"right\",\n                    onClose: () => {\n                        this.closePopoverFns = {};\n                    },\n                    closeOnClickAway: (target) => !target.closest(\".modal\"),\n                    popoverClass: \"sign-popover\",\n                }\n            );\n            this.closePopoverFns[signItem.data.id] = {\n                close: closeFn,\n                signItem,\n            };\n        }\n    }\n\n    /**\n     * Closes all open popovers\n     */\n    closePopover() {\n        if (Object.keys(this.closePopoverFns)) {\n            for (const id in this.closePopoverFns) {\n                this.closePopoverFns[id].close();\n            }\n            this.closePopoverFns = {};\n        }\n    }\n\n    /**\n     * Updates the radio set, re-renders it and saves the template in case there were changes.\n     * @param {Number} sign_item_id\n     * @param {Object} data\n     */\n    async updateRadioButton(sign_item_id, data) {\n        const signItem = this.getSignItemById(sign_item_id);\n        const { radio_set_id, num_options, responsible, required, placeholder } = signItem.data;\n        if (num_options != data.num_options){\n            await this.updateRadioNumOptions(radio_set_id, Number(data.num_options));\n        }\n        let changes = {};\n        if (responsible != data.responsible) {\n            changes['responsible'] = Number(data.responsible);\n        }\n        if (required != data.required) {\n            changes['required'] = data.required;\n        }\n        if (placeholder != data.placeholder) {\n            changes['placeholder'] = data.placeholder;\n        }\n        this.updateRadioSet(radio_set_id, changes);\n    }\n\n    /**\n     * Updates the sign item, re-renders it and saves the template in case there were changes\n     * @param {SignItem} signItem\n     * @param {Object} data\n     */\n    updateSignItem(signItem, data) {\n        if(signItem.data.type == \"radio\"){\n            return this.updateRadioButton(signItem.data.id, data);\n        }\n        const changes = Object.keys(data).reduce((changes, key) => {\n            if (key in signItem.data) {\n                if (Array.isArray(data[key])) {\n                    if (!shallowEqual(signItem.data[key], data[key])) {\n                        changes[key] = data[key];\n                    }\n                } else if (signItem.data[key] !== data[key]) {\n                    changes[key] = data[key];\n                }\n            }\n            return changes;\n        }, {});\n\n        if (Object.keys(changes).length) {\n            const pageNumber = signItem.data.page;\n            const page = this.getPageContainer(pageNumber);\n            signItem.el.parentElement.removeChild(signItem.el);\n            const newData = {\n                ...signItem.data,\n                ...changes,\n                updated: true,\n            };\n            this.signItems[pageNumber][newData.id] = {\n                data: newData,\n                el: this.renderSignItem(newData, page),\n            };\n            this.refreshSignItems();\n            this.currentRole = newData.responsible;\n            this.saveChanges();\n        }\n    }\n\n    /**\n     * Deletes a radio set from the template\n     * @param {radio_set_id}\n     */\n    deleteRadioSet(radio_set_id) {\n        const radioSet = this.radioSets[radio_set_id];\n        radioSet.radio_item_ids.forEach((id) => {\n            const signItem = this.getSignItemById(id);\n            signItem.el.parentElement.removeChild(signItem.el);\n            delete this.signItems[signItem.data.page][id];\n        })\n        delete this.radioSets[radio_set_id];\n        this.renderAllConnectingLines();\n        this.orm.unlink('sign.item.radio.set', [radio_set_id]);\n    }\n\n    /**\n     * Deletes a sign item from the template\n     * @param {SignItem} signItem\n     */\n    deleteSignItem(signItem) {\n        if (signItem.data.type == \"radio\"){\n            return this.deleteRadioSet(this.getSignItemById(signItem.data.id).data.radio_set_id);\n        }\n        this.deletedSignItemIds.push(signItem.data.id);\n        super.deleteSignItem(signItem);\n    }\n\n    /**\n     * Enables resizing and drag/drop for sign items\n     * @param {SignItem} signItem\n     */\n    enableCustom(signItem) {\n        if (this.allowEdit) {\n            startResize(signItem, this.onResizeItem.bind(this));\n            this.registerDragEventsForSignItem(signItem);\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        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 = signItem.responsible ?? (signItem.responsible_id?.[0] || 0);\n        const type = this.signItemTypesById[signItem.type_id[0]].item_type;\n        if (type === \"selection\") {\n            const options = signItem.option_ids.map((id) => this.selectionOptionsById[id]);\n            signItem.options = options;\n        }\n        return Object.assign(signItem, {\n            readonly: true,\n            editMode: true,\n            required: Boolean(signItem.required),\n            responsible,\n            type,\n            placeholder: signItem.placeholder || signItem.name || \"\",\n            responsibleName: this.signRolesById[responsible].name,\n            classes: `o_color_responsible_${this.signRolesById[responsible].color} o_readonly_mode`,\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     * Hook executed before rendering the sign items and the sidebar\n     */\n    preRender() {\n        super.preRender();\n        if (this.allowEdit && !isMobileOS()) {\n            const outerContainer = this.root.querySelector(\"#outerContainer\");\n            Object.assign(outerContainer.style, {\n                width: \"auto\",\n                marginLeft: \"14rem\",\n            });\n            outerContainer.classList.add(\"o_sign_field_type_toolbar_visible\");\n            this.root.dispatchEvent(new Event(\"resize\"));\n        } else if (!this.allowEdit) {\n            const div = this.root.createElement(\"div\");\n            Object.assign(div.style, {\n                position: \"absolute\",\n                top: 0,\n                left: 0,\n                width: \"100%\",\n                height: \"100%\",\n                zIndex: 110,\n                opacity: 0.75,\n            });\n            this.root.querySelector(\"#viewer\").style.position = \"relative\";\n            this.root.querySelector(\"#viewer\").prepend(div);\n        }\n        this.insertRotatePDFButton();\n    }\n\n    insertRotatePDFButton() {\n        const printButton = this.root.querySelector(\"#print\");\n        const button = this.root.createElement(\"button\");\n        button.setAttribute(\"id\", \"pageRotateCw\");\n        button.className = \"toolbarButton o_sign_rotate rotateCw\";\n        button.title = _t(\"Rotate Clockwise\");\n        printButton.parentNode.insertBefore(button, printButton);\n        button.addEventListener(\"click\", (e) => this.rotatePDF(e));\n    }\n\n    postRender() {\n        super.postRender();\n        if (this.allowEdit) {\n            const viewerContainer = this.root.querySelector(\"#viewerContainer\");\n            // close popover when clicking outside of a sign item\n            viewerContainer.addEventListener(\n                \"click\",\n                (e) => {\n                    if (!e.target.closest(\".o_sign_item_display\")) {\n                        this.closePopover();\n                    }\n                },\n                { capture: true }\n            );\n            this.root.addEventListener(\"keyup\", (e) => this.handleKeyUp(e));\n        }\n    }\n\n    handleKeyUp(e) {\n        if (e.key === \"Delete\" && Object.keys(this.closePopoverFns)) {\n            //delete any element that has its popover open\n            for (const id in this.closePopoverFns) {\n                const { close, signItem } = this.closePopoverFns[id];\n                typeof close === \"function\" && close();\n                this.deleteSignItem(signItem);\n            }\n            this.closePopoverFns = {};\n        }\n    }\n\n    async saveChanges() {\n        const items = this.signItems;\n        for (const page in items) {\n            for (const id in items[page]) {\n                const signItem = items[page][id].data;\n                if (signItem.id < 0) {\n                    this.negativeIds[id] = new Deferred();\n                }\n            }\n        }\n        let new_radio_items = []\n        const Id2UpdatedItem = await this.props.saveTemplate();\n        Object.entries(Id2UpdatedItem).forEach(([previousId, { page, id }]) => {\n            if (Number(previousId) !== id && this.signItems[page][previousId]) {\n                const prevEl = this.signItems[page][previousId].el;\n                const prevData = this.signItems[page][previousId].data;\n                this.signItems[page][id] = {\n                    data: prevData,\n                    el: prevEl,\n                };\n                this.negativeIds[previousId]?.resolve();\n                delete this.negativeIds[previousId];\n                delete this.signItems[page][previousId];\n                this.signItems[page][id].el.dataset.id = id;\n                if (prevData.type == \"radio\") {\n                    new_radio_items.push({'page': page,'id': id});\n                }\n            }\n            this.signItems[page][id].data.updated = false;\n        });\n        // Assign the radio_set info to the newely created radio items\n        if (new_radio_items.length) {\n            const info = await this.props.getRadioSetInfo(new_radio_items.map((obj) => obj.id));\n            new_radio_items.forEach(({page, id}) => {\n                const { data, el } = this.signItems[page][id];\n                const updatedData = Object.assign({}, data, info[id]);\n                const {radio_set_id, num_options} = info[id];\n                if (!this.radioSets[radio_set_id]) {\n                    this.radioSets[radio_set_id] = {\n                        num_options: 0,\n                        radio_item_ids: [],\n                    }\n                }\n                this.radioSets[radio_set_id].num_options = num_options;\n                this.radioSets[radio_set_id].radio_item_ids.push(id);\n                this.signItems[page] = {\n                  ...this.signItems[page],\n                  [id]: {\n                    data: updatedData,\n                    el: el,\n                  },\n                };\n            });\n        }\n        this.deletedSignItemIds = [];\n        this.renderAllConnectingLines();\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     * Updates the local selection options to include the new records\n     * @param {Array<Number>} optionIds\n     */\n    async updateSelectionOptions(optionIds) {\n        const newIds = optionIds.filter((id) => !(id in this.selectionOptionsById));\n        const newOptions = await this.orm.searchRead(\n            \"sign.item.option\",\n            [[\"id\", \"in\", newIds]],\n            [\"id\", \"value\"],\n            { context: user.context }\n        );\n        for (const option of newOptions) {\n            this.selectionOptionsById[option.id] = option;\n        }\n    }\n\n    /**\n     * Creates #count new sign radio items and chains them vertically to the lastSignItem\n     * by increasing of Y position, renders them and saves the template.\n     * @param {SignItem} lastSignItem\n     * @param {Number} count\n     */\n    async render_new_radio_items(lastSignItem, count) {\n        let tail = lastSignItem;\n        for(let i = 0; i < count; i++){\n            const new_id = generateRandomId();\n            const new_data = { ...tail.data };\n            new_data['id'] = new_id;\n            new_data['posY'] += 0.025;\n            this.signItems[new_data.page][new_id] = {\n                data: new_data,\n                el: this.renderSignItem(new_data, this.getPageContainer(new_data.page)),\n            }\n            tail = this.signItems[new_data.page][new_id];\n            new_data.updated = true;\n        }\n        this.refreshSignItems();\n        await this.saveChanges();\n    }\n\n    /**\n     * Removes the last #count sign items from radio_item_ids from the template and saves the changes.\n     * @param {Number[]} radio_item_ids\n     * @param {Number} count\n     */\n    async deleteRadioItems(radio_item_ids, count) {\n        let deleted_sign_items = [];\n        for (let i = 0; i < count; i++) {\n            const id = radio_item_ids[radio_item_ids.length - 1 - i];\n            const signItem = this.getSignItemById(id);\n            deleted_sign_items.push(signItem);\n        }\n        await this.deleteSignItems(deleted_sign_items);\n    }\n\n    /**\n     * Apply changes to the entire radio set, re-renders sign items and saved the changes.\n     * @param {Number} radio_set_id\n     * @param {Object} changes\n     * @returns \n     */\n    async updateRadioSet(radio_set_id, changes) {\n        if(Object.keys(changes).length === 0) {\n            return;\n        }\n        const radio_set = this.radioSets[radio_set_id];\n        radio_set.radio_item_ids.forEach((id) => {\n            const signItem = this.getSignItemById(id);\n            const pageNumber = signItem.data.page;\n            const page = this.getPageContainer(pageNumber);\n            signItem.el.parentElement.removeChild(signItem.el);\n            const newData = {\n                ...signItem.data,\n                ...changes,\n                updated: true,\n            };\n            this.signItems[pageNumber][id] = {\n                data: newData,\n                el: this.renderSignItem(newData, page),\n            };\n        });\n        this.refreshSignItems();\n        await this.saveChanges();\n    }\n\n    /**\n     * Changes radio set number of options, renders new items, deletes extra items and saves changes.\n     * @param {Number} radio_set_id\n     * @param {Number} count\n     */\n    async updateRadioNumOptions(radio_set_id, count) {\n        const radio_set = this.radioSets[radio_set_id];\n        if (radio_set.num_options < count) {\n            const signItem = this.getSignItemById(radio_set.radio_item_ids[radio_set.num_options - 1]);\n            await this.render_new_radio_items(signItem, count - radio_set.num_options);\n        }else if (radio_set.num_options > count) {\n            await this.deleteRadioItems(radio_set.radio_item_ids, radio_set.num_options - count);\n        }\n        const new_radio_set = this.radioSets[radio_set_id];\n        new_radio_set.radio_item_ids.forEach((id) => {\n            const signItem = this.getSignItemById(id);\n            signItem.data.num_options = new_radio_set.num_options;\n        })\n    }\n\n    /**\n     * Updates the local roles to include new records\n     * @param {Number} id role id\n     */\n    async updateRoles(id) {\n        if (!(id in this.signRolesById)) {\n            const newRole = await this.orm.searchRead(\"sign.item.role\", [[\"id\", \"=\", id]], []);\n            this.signRolesById[newRole[0].id] = newRole[0];\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Record } from \"@web/model/record\";\nimport {\n    many2ManyTagsField,\n    Many2ManyTagsField,\n    Many2ManyTagsFieldColorEditable,\n} from \"@web/views/fields/many2many_tags/many2many_tags_field\";\nimport { Component, useRef, useState } from \"@odoo/owl\";\n\nconst actionFieldsGet = {\n    tag_ids: { type: \"many2many\", relation: \"sign.template.tag\", string: \"Tags\" },\n    authorized_ids: { type: \"many2many\", relation: \"res.users\", string: \"Authorized Users\" },\n    group_ids: { type: \"many2many\", relation: \"res.groups\", string: \"Authorized Groups\" },\n};\n\nfunction getActionActiveFields() {\n    const activeFields = {};\n    for (const fName of Object.keys(actionFieldsGet)) {\n        const related = Object.fromEntries(\n            many2ManyTagsField.relatedFields({ options: {} }).map((f) => [f.name, f])\n        );\n        activeFields[fName] = {\n            related: {\n                activeFields: related,\n                fields: related,\n            },\n        };\n    }\n    activeFields.tag_ids.related.activeFields.color = { type: \"integer\", string: \"Color\" };\n    return activeFields;\n}\n\nexport class SignTemplateTopBar extends Component {\n    static template = \"sign.SignTemplateTopBar\";\n    static components = {\n        Many2ManyTagsFieldColorEditable,\n        Many2ManyTagsField,\n        Record,\n    };\n    static props = {\n        signTemplate: { type: Object },\n        hasSignRequests: { type: Boolean },\n        onTemplateNameChange: { type: Function },\n        manageTemplateAccess: { type: Boolean },\n        resModel: { type: String },\n        isPDF: { type: Boolean },\n    };\n\n    setup() {\n        this.displayNameInput = useRef(\"display-name\");\n        this.action = useService(\"action\");\n        this.notification = useService(\"notification\");\n        this.orm = useService(\"orm\");\n        this.signTemplateFieldsGet = getActionActiveFields();\n        this.state = useState({\n            properties: false,\n        });\n    }\n\n    changeInputSize() {\n        const el = this.displayNameInput.el;\n        if (el) {\n            el.size = el.value.length + 1;\n        }\n    }\n\n    get displayNameSize() {\n        return this.props.signTemplate.display_name.length + 1;\n    }\n\n    editDisplayName() {\n        this.displayNameInput.el.focus();\n        this.displayNameInput.el.select();\n    }\n\n    onKeyDown(e) {\n        if (e.key === \"Enter\") {\n            this.displayNameInput.el.blur();\n        }\n    }\n\n    /**\n    * Saves the current signature document as sign template,\n    * and updates the button's property state accordingly.\n    *\n    * @returns {Promise|boolean}\n    */\n    async onTemplateSaveClick() {\n        const templateId = this.props.signTemplate.id;\n        this.state.properties = await this.orm.call(\"sign.template\", \"write\", [[templateId], { active: true }]);\n        this.props.signTemplate.active = true;\n        this.notification.add(_t(\"Document saved as Template.\"), { type: \"success\" });\n        return this.state.properties;\n    }\n\n    onTemplatePropertiesClick() {\n        this.action.doAction({\n            name: \"Edit Template Form\",\n            type: \"ir.actions.act_window\",\n            res_model: \"sign.template\",\n            res_id: this.props.signTemplate.id,\n            views: [[false, \"form\"]],\n        });\n    }\n\n    getMany2ManyProps(record, fieldName) {\n        return {\n            name: fieldName,\n            id: fieldName,\n            record,\n            readonly: this.props.hasSignRequests,\n        };\n    }\n\n    get recordProps() {\n        return {\n            mode: this.props.hasSignRequests ? \"readonly\" : \"edit\",\n            onRecordChanged: (record, changes) => {\n                this.saveChanges(record, changes);\n            },\n            resModel: \"sign.template\",\n            resId: this.props.signTemplate.id,\n            fieldNames: this.signTemplateFieldsGet,\n            activeFields: this.signTemplateFieldsGet,\n        };\n    }\n\n    async saveChanges(record, changes) {\n        const result = await this.orm.write(\"sign.template\", [record.resId], changes);\n        return result;\n    }\n\n    get showEditButton() {\n        return this.props.hasSignRequests && this.props.isPDF;\n    }\n\n    async editTemplate() {\n        const duplicatedTemplateIds = await this.orm.call(\"sign.template\", \"copy\", [\n            [this.props.signTemplate.id],\n        ]);\n\n        this.action.doAction({\n            type: \"ir.actions.client\",\n            tag: \"sign.Template\",\n            name: _t(\"Edit Template\"),\n            params: {\n                id: duplicatedTemplateIds[0],\n            },\n        });\n    }\n\n}\n", "/** @odoo-module **/\n\nimport { ConfirmationDialog, AlertDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\n\nexport class TemplateAlertDialog extends AlertDialog {\n    static props = {\n        ...AlertDialog.props,\n        body: { type: String, optional: true },\n        failedTemplates: { type: Array },\n        successTemplateCount: { type: Number, optional: true },\n    };\n    static defaultProps = {\n        ...ConfirmationDialog.defaultProps,\n        ...AlertDialog.defaultProps,\n    };\n    static template = \"sign.TemplateAlertDialog\";\n}\n", "import { useSignViewButtons } from \"@sign/views/hooks\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useRef, Component } from \"@odoo/owl\";\n\nexport class SignActionHelper extends Component {\n    static template = \"sign.SignActionHelper\";\n    static props = [\n        \"resModel\"\n    ];\n\n    setup() {\n        super.setup(...arguments);\n        this.actionService = useService(\"action\");\n        this.fileInput = useRef(\"uploadFileInput\");\n        const functions = useSignViewButtons();\n        Object.assign(this, functions);\n    }\n\n    onClickUpload(context) {\n        return this.requestFile(context);\n    }\n\n    onClicksampleSign() {\n        return this.actionService.doAction(\"sign.sign_template_tour_trigger_action\");\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService, useBus } from \"@web/core/utils/hooks\";\nimport { multiFileUpload } from \"@sign/backend_components/multi_file_upload\";\nimport { user } from \"@web/core/user\";\nimport { getDataURLFromFile } from \"@web/core/utils/urls\";\nimport { TemplateAlertDialog } from \"@sign/backend_components/template_alert_dialog/template_alert_dialog\";\nimport { onWillStart, useComponent, useRef, useEnv } from \"@odoo/owl\";\n\nexport function useSignViewButtons() {\n    const component = useComponent();\n    const fileInput = useRef(\"uploadFileInput\");\n    const orm = useService(\"orm\");\n    const dialog = useService(\"dialog\");\n    const action = useService(\"action\");\n    const env = useEnv();\n\n    onWillStart(async () => {\n        component.isSignUser = await user.hasGroup(\"sign.group_sign_user\");\n    });\n\n    let latestRequestContext;\n    let inactive;\n    let resModel;\n\n    const uploadFile = async (file) => {\n        const dataUrl = await getDataURLFromFile(file);\n        inactive = resModel === 'sign.template' ? true : false;\n        const args = [file.name, dataUrl.split(\",\")[1], inactive];\n        return await orm.call(\"sign.template\", \"create_with_attachment_data\", args);\n    };\n\n    /**\n     * Called on newly created templates, does a sign.Template action on those.\n     */\n    const handleTemplates = (templates) => {\n        if (!templates || !templates.length) {\n            return;\n        }\n        const file = templates.shift();\n        multiFileUpload.addNewFiles(templates);\n        action.doAction({\n            type: \"ir.actions.client\",\n            tag: \"sign.Template\",\n            name: _t(\"Template %s\", file.name),\n            params: {\n                sign_edit_call: latestRequestContext,\n                id: file.template,\n                sign_directly_without_mail: false,\n                resModel: resModel,\n            },\n        });\n    };\n\n    const upload = {\n        /**\n         * Handles the template file upload logic.\n         */\n        onFileInputChange: async (ev) => {\n            const files = ev?.type === \"change\" ? ev.target.files : ev.detail.files;\n            if (!files || !files.length) {\n                return;\n            }\n            const uploadedTemplates = await Promise.all(Array.from(files).map(uploadFile));\n            const templates = uploadedTemplates.map((template, index) => ({\n                template,\n                name: files[index].name,\n            }));\n            const { true: succesfulTemplates, false: failedTemplates } = templates.reduce(\n                (result, item) => {\n                    const key = Boolean(item.template);\n                    if (!result[key]) {\n                        result[key] = [];\n                    }\n                    result[key].push(item);\n                    return result;\n                },\n                {}\n            );\n            if (failedTemplates && failedTemplates.length) {\n                dialog.add(TemplateAlertDialog, {\n                    title: _t(\"File Error\"),\n                    failedTemplates,\n                    successTemplateCount: succesfulTemplates && succesfulTemplates.length,\n                    confirm: handleTemplates.bind(undefined, succesfulTemplates),\n                });\n            } else {\n                handleTemplates(succesfulTemplates);\n            }\n            ev.target.value = \"\";\n        },\n\n        /**\n        * Initiates the file upload process by opening a file input dialog\n        * and configuring the 'save as template' button based on the provided model\n        * and other properties.\n        *\n        * @param {Object}\n        */\n        requestFile(context) {\n            latestRequestContext = context;\n            resModel = this.props.resModel;\n            fileInput.el.click();\n        },\n    };\n\n    useBus(env.bus, \"change_file_input\", async (ev) => {\n        fileInput.el.files = ev.detail.files;\n        resModel = ev.detail.resModel\n        await upload.onFileInputChange(ev);\n    });\n\n    return upload\n}\n", "/** @odoo-module **/\n\nimport { KanbanController } from \"@web/views/kanban/kanban_controller\";\nimport { useSignViewButtons } from \"@sign/views/hooks\";\nimport { Dropdown, DropdownItem } from \"@web/core/dropdown/dropdown\";\n\nexport class SignKanbanController extends KanbanController {\n    static template = \"sign.SignKanbanController\";\n    static components = {\n        ...KanbanController.components,\n        Dropdown,\n        DropdownItem,\n    };\n\n    setup() {\n        super.setup(...arguments);\n        const functions = useSignViewButtons();\n        Object.assign(this, functions);\n    }\n}\n", "/** @odoo-module **/\n\nimport { KanbanRenderer } from \"@web/views/kanban/kanban_renderer\";\nimport { SignDocumentDropZone } from '../../mixin/document_upload';\nimport { SignActionHelper } from '@sign/views/helper/sign_action_helper';\n\nexport class SignKanbanRenderer extends SignDocumentDropZone(KanbanRenderer) {\n    static template = \"sign.KanbanRenderer\";\n    static components = { \n        ...KanbanRenderer.components,\n        SignActionHelper,\n    };\n\n    /**\n     * @override\n     * Prevent moving records for sign request items\n     */\n    get canMoveRecords() {\n        return super.canMoveRecords && this.props.list.resModel !== \"sign.request\";\n    }\n\n    /**\n     * @override\n     * Prevent moving groups for sign request items\n     */\n    get canResequenceGroups() {\n        return super.canResequenceGroups && this.props.list.resModel !== \"sign.request\";\n    }\n}\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { kanbanView } from \"@web/views/kanban/kanban_view\";\nimport { SignKanbanController } from \"@sign/views/sign_kanban/sign_kanban_controller\";\nimport { SignKanbanRenderer } from \"@sign/views/sign_kanban/sign_kanban_renderer\";\n\nexport const signKanbanView = {\n    ...kanbanView,\n    Controller: SignKanbanController,\n    Renderer: SignKanbanRenderer,\n};\nregistry.category(\"views\").add(\"sign_kanban\", signKanbanView);\n", "/** @odoo-module **/\n\nimport { ListController } from \"@web/views/list/list_controller\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { useSignViewButtons } from \"@sign/views/hooks\";\nimport { Dropdown, DropdownItem } from \"@web/core/dropdown/dropdown\";\nimport { SignDocumentDropZone } from \"../../mixin/document_upload\";\nimport { SignActionHelper } from '@sign/views/helper/sign_action_helper';\n\nexport class SignListController extends ListController {\n    static template = \"sign.SignListController\";\n    static components = {\n        ...ListController.components,\n        Dropdown,\n        DropdownItem,\n    };\n\n    setup() {\n        super.setup(...arguments);\n        const functions = useSignViewButtons();\n        Object.assign(this, functions);\n    }\n}\n\nexport class SignListRenderer extends SignDocumentDropZone(ListRenderer)  {\n    static template = \"sign.ListRenderer\";\n    static components = {\n        ...ListRenderer.components,\n        SignActionHelper,\n    };\n}\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { listView } from \"@web/views/list/list_view\";\nimport { SignListController, SignListRenderer } from \"@sign/views/sign_list/sign_list_controller\";\n\nexport const signListView = {\n    ...listView,\n    Controller: SignListController,\n    Renderer: SignListRenderer,\n};\nregistry.category(\"views\").add(\"sign_list\", signListView);\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        onSignatureChange: { type: Function, optional: true },\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            this.props.onSignatureChange(this.state.signMode);\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        this.props.onSignatureChange(this.state.signMode);\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            signButtonDisabled: !this.props.signature.name || !this.props.signature.signatureChanged,\n            signAllButtonsDisabled: !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            onSignatureChange: this.onSignatureChange.bind(this),\n        };\n    }\n\n    get dialogProps() {\n        return {\n            title: _t(\"Adopt Your Signature\"),\n            size: \"md\",\n        };\n    }\n\n    onNameChange(name) {\n        const isNameFilled = Boolean(name);\n        this.footerState.signButtonDisabled = !isNameFilled;\n        this.footerState.signAllButtonsDisabled = !isNameFilled;\n    }\n\n    onSignatureChange(signMode) {\n        const signature = this.props.signature;\n        const isNotValidSignature = !signature.name || (signature.isSignatureEmpty && (!signMode || signMode !== \"auto\"));\n        this.footerState.signAllButtonsDisabled = isNotValidSignature;\n        this.footerState.signButtonDisabled = isNotValidSignature || !signature.signatureChanged;\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\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\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 get the signed document by email.\");\n        onWillStart(this.willStart);\n        this.isMobileOS = isMobileOS();\n    }\n\n    get suggestSignUp() {\n        return !user.userId;\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                classes: 'o_sign_thankyou_redirect_button',\n            });\n        }\n        if (!this.redirectURL) {\n            this.state.buttons.push({\n                name: this.closeLabel,\n                click: () => {\n                    if (this.suggestSignUp) {\n                        window.open(`https://odoo.com/app/sign`);\n                    }\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                classes: 'o_sign_thankyou_close_button'\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 = \"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 { registry } from \"@web/core/registry\";\nimport { Many2ManyBinaryField, many2ManyBinaryField } from \"@web/views/fields/many2many_binary/many2many_binary_field\";\n\nexport class Many2ManyBinaryFieldSignRequest extends Many2ManyBinaryField {\n    static template = \"sign.Many2ManyBinaryField\";\n}\n\nexport const many2ManyBinaryFieldSignRequest = {\n    ...many2ManyBinaryField,\n    component: Many2ManyBinaryFieldSignRequest,\n};\n\nregistry.category(\"fields\").add(\"many2many_binary_sign_request\", many2ManyBinaryFieldSignRequest);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { X2ManyField, x2ManyField } from \"@web/views/fields/x2many/x2many_field\";\nimport { Many2OneField } from \"@web/views/fields/many2one/many2one_field\";\nimport { IntegerField } from \"@web/views/fields/integer/integer_field\";\n\nexport class SignerX2Many extends X2ManyField {\n    static template = \"sign.SignerX2Many\";\n    static components = {\n        ...X2ManyField.components,\n        Many2OneField,\n        IntegerField,\n    };\n\n    static props = {\n        ...X2ManyField.props,\n    };\n\n    get partnerIdFieldInfo() {\n        return {\n            name: \"partner_id\",\n            additionalProps: {\n                readonly: false,\n                placeholder: _t(\"Type a name or email...\"),\n                context: { force_email: true, show_email: true },\n            },\n        };\n    }\n\n    get shouldShowOrder() {\n        return this.props.record.data[\"set_sign_order\"];\n    }\n}\n\nconst signerX2Many = {\n    component: SignerX2Many,\n    displayName: _t(\"Signer One 2 Many\"),\n    additionalClasses: [\"o_required_modifier\"],\n    supportedTypes: [\"one2many\"],\n    relatedFields: () => {\n        return [\n            { name: \"role_id\", type: \"many2one\", relation: \"sign.item.role\", readonly: false },\n            { name: \"partner_id\", type: \"many2one\", relation: \"res.partner\", readonly: false },\n            { name: \"mail_sent_order\", type: \"integer\", readonly: false },\n        ];\n    },\n    fieldDependencies: [{ name: \"set_sign_order\", type: \"boolean\" }],\n    extractProps: x2ManyField.extractProps,\n};\n\nregistry.category(\"fields\").add(\"signer_x2many\", signerX2Many);\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", "/** @odoo-module */\n\nimport { NotificationItem } from \"@mail/core/public_web/notification_item\";\n\nNotificationItem.props = [...NotificationItem.props, \"rating?\"];\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport {\n    Many2ManyTagsField,\n    many2ManyTagsField,\n} from \"@web/views/fields/many2many_tags/many2many_tags_field\";\n\nimport { HelpdeskSLATagsList } from \"../helpdesk_sla_tags_list/helpdesk_sla_tags_list\";\n\n\nclass HelpdeskSLAMany2ManyTags extends Many2ManyTagsField {\n    static components = { ...Many2ManyTagsField.components, TagsList: HelpdeskSLATagsList };\n    getTagProps(record) {\n        return { ...super.getTagProps(record), slaStatus: record.data.status };\n    }\n}\n\nexport const helpdeskSLAMany2ManyTags = {\n    ...many2ManyTagsField,\n    component: HelpdeskSLAMany2ManyTags,\n    relatedFields: (fieldInfo) => {\n        return [\n            ...many2ManyTagsField.relatedFields(fieldInfo),\n            { name: \"status\", type: \"selection\", selection: [] },\n        ];\n    },\n    additionalClasses: [...(many2ManyTagsField.additionalClasses || []), \"o_field_many2many_tags\"],\n};\n\nregistry.category(\"fields\").add(\"helpdesk_sla_many2many_tags\", helpdeskSLAMany2ManyTags);\n", "/** @odoo-module **/\n\nimport { TagsList } from \"@web/core/tags_list/tags_list\";\n\n\nexport class HelpdeskSLATagsList extends TagsList {\n    static template = \"helpdesk.SLATagsList\";\n\n    getSLAStatusIcon(tag) {\n        let iconType = \"\";\n        if (tag.slaStatus === \"failed\") {\n            iconType = \"times\";\n        } else if (tag.slaStatus === \"reached\") {\n            iconType = \"check\";\n        }\n        return iconType ? `fa fa-${iconType}-circle me-2`: \"\";\n    }\n\n}\n", "/** @odoo-module **/\n\nimport { user } from '@web/core/user';\nimport { formatFloatTime } from '@web/views/fields/formatters';\nimport { formatFloat } from \"@web/core/utils/numbers\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { HelpdeskTeamTarget } from \"../helpdesk_team_target/helpdesk_team_target\";\nimport { Component, useState, onWillStart } from \"@odoo/owl\";\n\nexport class HelpdeskTeamDashboard extends Component {\n    static template = \"helpdesk.HelpdeskTeamDashboard\";\n    static components = {\n        HelpdeskTeamTarget,\n    };\n    static props = {};\n\n    setup() {\n        this.action = useService('action');\n        this.orm = useService('orm');\n        this.state = useState({\n            dashboardValues: null,\n        });\n\n        onWillStart(this.onWillStart);\n    }\n\n    get showDemo() {\n        return Boolean(this.state.dashboardValues) && this.state.dashboardValues.show_demo;\n    }\n\n    get demoClass() {\n        return this.showDemo ? 'o_demo o_disabled o_cursor_default' : '';\n    }\n\n    async onWillStart() {\n        await this._fetchData();\n    }\n\n    async updateHelpdeskTarget(targetName, value) {\n        await this.orm.write(\n            'res.users',\n            [user.userId],\n            { [targetName]: value },\n        );\n        this.state.dashboardValues[targetName] = value;\n    }\n\n    /**\n     * @param {MouseEvent} e\n     */\n    async onActionClicked(e) {\n        if (this.showDemo) {\n            return;\n        }\n        const action = e.currentTarget;\n        const actionRef = action.getAttribute('name');\n        const title = action.dataset.actionTitle || action.getAttribute('title');\n        const searchViewRef = action.getAttribute('search_view_ref');\n        const buttonContext = action.getAttribute('context') || '';\n\n        if (action.getAttribute('name').includes('helpdesk.')) {\n            return await this.action.doActionButton({\n                resModel: 'helpdesk.ticket',\n                name: 'create_action',\n                args: JSON.stringify([actionRef, title, searchViewRef]),\n                context: '',\n                buttonContext,\n                type: 'object',\n            });\n        } else {\n            if (['action_view_rating_today', 'action_view_rating_7days'].includes(actionRef)) {\n                return this.action.doActionButton({\n                    resModel: 'helpdesk.team',\n                    name: actionRef,\n                    context: '',\n                    buttonContext,\n                    type: 'object',\n                });\n            }\n            return this.action.doAction(actionRef);\n        }\n    }\n\n    async _fetchData() {\n        this.state.dashboardValues = await this.orm.call(\n            'helpdesk.team',\n            'retrieve_dashboard',\n            [],\n            { context: user.context },\n        );\n    }\n\n    formatFloat(value, options = {}) {\n        return formatFloat(value, options);\n    }\n\n    formatTime(value, options = {}) {\n        return formatFloatTime(value, options);\n    }\n\n    parseInteger(value) {\n        return parseInt(value);\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { escapeRegExp } from \"@web/core/utils/strings\";\nimport { useAutofocus, useService } from \"@web/core/utils/hooks\";\nimport { formatFloat, formatPercentage } from \"@web/views/fields/formatters\";\nimport { Component, useRef, useState } from \"@odoo/owl\";\n\nexport class HelpdeskTeamTarget extends Component {\n    static template = \"helpdesk.HelpdeskTeamTarget\";\n    static props = {\n        showDemo: { type: Boolean, optional: true },\n        demoClass: { type: String, optional: true },\n        update: Function,\n        percentage: { type: Boolean, optional: true },\n        rating: { type: Boolean, optional: true },\n        value: Number,\n        hotkey: { type: String, optional: true },\n    };\n    static defaultProps = {\n        showDemo: false,\n        demoClass: \"\",\n        percentage: false,\n        rating: false,\n    };\n\n    setup() {\n        useAutofocus({ refName: 'inputRef', selectAll: true });\n        this.inputRef = useRef(\"inputRef\");\n        this.notification = useService(\"notification\");\n        this.state = useState({\n            isFocused: false,\n            value: this.props.value,\n        });\n    }\n\n    get valueString() {\n        return this.props.rating\n            ? `${this.state.value} / 5`\n            : !this.props.percentage\n            ? this.state.value\n            : formatPercentage(this.state.value / 100);\n    }\n\n    get helpdeskTeamTargetTitle() {\n        if (this.props.showDemo) {\n            return _t(\"Average rating daily target\");\n        } else if (this.props.rating) {\n            return _t(\"Click to Set Your Daily Rating Target\");\n        } else {\n            return _t(\"Click to set\");\n        }\n    }\n\n    /**\n     * @private\n     */\n    _toggleFocus() {\n        this.state.isFocused = !this.state.isFocused;\n    }\n\n    /**\n     * Handle the keydown event on the value input\n     *\n     * @private\n     * @param {KeyboardEvent} ev\n     */\n    _onInputKeydown(ev) {\n        if (ev.key === 'Enter') {\n            this.inputRef.el.blur();\n        }\n    }\n\n    /**\n     * @private\n     */\n    async _onValueChange() {\n        let inputValue = this.inputRef.el.value;\n        // a number can have the thousand separator multiple times. ex: 1,000,000.00\n        inputValue = inputValue.replaceAll(\n            new RegExp(escapeRegExp(localization.thousandsSep || \"\"), \"g\") || \",\",\n            \"\"\n        );\n        const targetValue = this.props.rating ? parseFloat(inputValue) : parseInt(inputValue);\n        if (Number.isNaN(targetValue)) {\n            this.notification.add(_t(\"Please enter a number.\"), { type: \"danger\" });\n            return;\n        }\n        if (targetValue <= 0) {\n            this.notification.add(_t(\"Please enter a positive value.\"), { type: \"danger\" });\n            return;\n        }\n        if (this.props.percentage && targetValue > 100) {\n            this.notification.add(_t(\"Please enter a percentage below 100.\"), { type: \"danger\" });\n            return;\n        } else if (this.props.rating && targetValue > 5) {\n            this.notification.add(_t(\"Please enter a value less than or equal to 5.\"), {\n                type: \"danger\",\n            });\n            return;\n        }\n        this.state.value = formatFloat(targetValue, { digits: [1, 0] });\n        await this.props.update(targetValue);\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { BadgeField } from \"@web/views/fields/badge/badge_field\";\n\nexport class HelpdeskSmileBadge extends BadgeField {\n    static template = \"helpdesk.SmileyBadge\";\n}\n\nexport const helpdeskSmileBadge = {\n    component: HelpdeskSmileBadge,\n};\n\nregistry.category(\"fields\").add(\"helpdesk_smiley_badge\", helpdeskSmileBadge);\n", "/** @odoo-module */\n\nimport { FormController } from \"@web/views/form/form_controller\";\nimport { onMounted, onWillStart, onPatched } from \"@odoo/owl\";\n\nexport class HelpdeskTeamController extends FormController {\n    setup() {\n        super.setup();\n        this.reloadInstall = false;\n        this.fieldsToObserve = {};\n        this.featuresToObserve = {};\n        this.featuresToCheck = [];\n        this.helpdeskResId = this.props.resId;\n        onWillStart(this.onWillStart);\n        onMounted(() => {\n            this.updateFieldsToObserve();\n        });\n        onPatched(this.onPatched);\n    }\n\n    async onWillStart() {\n        this.featuresToObserve = await this.orm.call(\n            this.modelParams.config.resModel,\n            \"check_features_enabled\",\n            []\n        );\n    }\n\n    onPatched() {\n        if (this.helpdeskResId !== this.model.root.resId) {\n            this.helpdeskResId = this.model.root.resId;\n            this.fieldsToObserve = {};\n            this.updateFieldsToObserve();\n        }\n    }\n\n    updateFieldsToObserve() {\n        for (const [fieldName, value] of Object.entries(this.model.root.data)) {\n            if (fieldName.startsWith(\"use_\")) {\n                this.fieldsToObserve[fieldName] = value;\n            }\n        }\n    }\n\n    /**\n     *\n     * @override\n     */\n    async onWillSaveRecord(record, changes) {\n        const res = await super.onWillSaveRecord(...arguments);\n        const fields = [];\n        for (const [fName, value] of Object.entries(changes)) {\n            if (value && fName in this.fieldsToObserve && this.fieldsToObserve[fName] !== value) {\n                fields.push(fName);\n            }\n            if (fName in this.featuresToObserve && value !== this.featuresToObserve[fName]) {\n                this.featuresToCheck.push(fName);\n            }\n        }\n        if (Object.keys(fields).length) {\n            this.reloadInstall = await record.model.orm.call(\n                record.resModel,\n                \"check_modules_to_install\",\n                [fields]\n            );\n        }\n        return res;\n    }\n\n    /**\n     * @override\n     */\n    async onRecordSaved(record) {\n        await super.onRecordSaved(...arguments);\n        let updatedEnabledFeatures = {};\n        if (!this.reloadInstall && this.featuresToCheck.length) {\n            updatedEnabledFeatures = await record.model.orm.call(\n                record.resModel,\n                \"check_features_enabled\",\n                [this.featuresToCheck]\n            );\n        }\n        if (\n            this.reloadInstall ||\n            Object.entries(updatedEnabledFeatures).some(\n                ([fName, value]) => value !== this.featuresToObserve[fName]\n            )\n        ) {\n            this.reloadInstall = false;\n            this.model.action.doAction(\"reload_context\");\n        }\n        this.updateFieldsToObserve();\n        this.featuresToCheck = [];\n    }\n}\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { formView } from \"@web/views/form/form_view\";\nimport { HelpdeskTeamController } from \"./helpdesk_team_form_controller\";\n\nregistry.category(\"views\").add(\"helpdesk_team_form\", {\n    ...formView,\n    Controller: HelpdeskTeamController,\n});\n", "/** @odoo-module **/\n\nimport { KanbanController } from '@web/views/kanban/kanban_controller';\nimport { HelpdeskTeamDashboard } from '@helpdesk/components/helpdesk_team_dashboard/helpdesk_team_dashboard';\n\nexport class HelpdeskTeamKanbanController extends KanbanController {\n    static template = \"helpdesk.HelpdeskTeamKanbanView\";\n    static components = {\n        ...KanbanController.components,\n        HelpdeskTeamDashboard,\n    };\n}\n", "import { registry } from '@web/core/registry';\nimport { kanbanView } from '@web/views/kanban/kanban_view';\nimport { HelpdeskTeamKanbanController } from './helpdesk_team_kanban_controller';\n\nexport const HelpdeskTeamKanbanView = {\n    ...kanbanView,\n    Controller: HelpdeskTeamKanbanController,\n    searchview_hidden: true,\n};\n\nregistry.category('views').add('helpdesk_team_kanban_view', HelpdeskTeamKanbanView);\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { KanbanHeader } from \"@web/views/kanban/kanban_header\";\n\nexport class HelpdeskTicketKanbanHeader extends KanbanHeader {\n    setup() {\n        super.setup();\n        this.action = useService(\"action\");\n    }\n\n    /**\n     * @override\n     */\n    _getEmptyGroupLabel(fieldName) {\n        if (fieldName === \"sla_deadline\") {\n            return _t(\"Deadline reached\");\n        } else {\n            return super._getEmptyGroupLabel(fieldName);\n        }\n    }\n\n    async deleteGroup() {\n        if (this.group.groupByField.name === \"stage_id\") {\n            const action = await this.group.model.orm.call(\n                this.group.groupByField.relation,\n                \"action_unlink_wizard\",\n                [this.group.value],\n                { context: this.group.context }\n            );\n            this.action.doAction(action);\n            return;\n        }\n        super.deleteGroup();\n    }\n}\n", "/** @odoo-module */\n\nimport { KanbanRenderer } from '@web/views/kanban/kanban_renderer';\nimport { HelpdeskTicketKanbanHeader } from './helpdesk_ticket_kanban_header';\nimport { onWillStart } from \"@odoo/owl\";\nimport { user } from \"@web/core/user\";\n\nexport class HelpdeskTicketRenderer extends KanbanRenderer {\n    static components = {\n        ...KanbanRenderer.components,\n        KanbanHeader: HelpdeskTicketKanbanHeader,\n    };\n\n    setup() {\n        super.setup();\n        onWillStart(async () => {\n            this.isHelpdeskManager = await user.hasGroup('helpdesk.group_helpdesk_manager');\n        });\n    }\n\n    canCreateGroup() {\n        return super.canCreateGroup() && !!this.props.list.context.default_team_id;\n    }\n\n    get canResequenceGroups() {\n        return super.canResequenceGroups && this.isHelpdeskManager;\n    }\n}\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { kanbanView } from '@web/views/kanban/kanban_view';\nimport { HelpdeskTicketRenderer } from './helpdesk_ticket_kanban_renderer';\n\nexport const helpdeskTicketKanbanView = {\n    ...kanbanView,\n    Renderer: HelpdeskTicketRenderer,\n};\n\nregistry.category('views').add('helpdesk_ticket_kanban', helpdeskTicketKanbanView);\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\n\nexport class HelpdeskTicketListRenderer extends ListRenderer {\n    getGroupDisplayName(group) {\n        if (group.groupByField.name === \"sla_deadline\" && !group.value) {\n            return _t(\"Deadline reached\");\n        } else {\n            return super.getGroupDisplayName(group);\n        }\n    }\n}\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { listView } from '@web/views/list/list_view';\nimport { HelpdeskTicketListRenderer } from \"./helpdesk_ticket_list_renderer\";\n\nexport const helpdeskTicketListView = {\n    ...listView,\n    Renderer: HelpdeskTicketListRenderer,\n};\n\nregistry.category(\"views\").add(\"helpdesk_ticket_list\", helpdeskTicketListView);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { markup } from \"@odoo/owl\";\n\nregistry.category(\"web_tour.tours\").add(\"helpdesk_tour\", {\n    url: \"/odoo\",\n    steps: () => [\n        {\n    trigger: '.o_app[data-menu-xmlid=\"helpdesk.menu_helpdesk_root\"]',\n    content: markup(_t('Want to <b>boost your customer satisfaction</b>?<br/><i>Click Helpdesk to start.</i>')),\n    tooltipPosition: 'bottom',\n            run: \"click\",\n        },\n        {\n            trigger: \".o_helpdesk_ticket_btn\",\n        },\n        {\n    trigger: 'button.oe_kanban_action',\n    content: markup(_t('Let\\'s view your <b>team\\'s tickets</b>.')),\n    tooltipPosition: 'bottom',\n            run: \"click\",\n        },\n        {\n            trigger: \".o_helpdesk_ticket_kanban_view\",\n        },\n        {\n    trigger: '.o-kanban-button-new',\n    content: markup(_t('Let\\'s create your first <b>ticket</b>.')),\n    tooltipPosition: 'bottom',\n            run: \"click\",\n        },\n        {\n            trigger: \".o_form_editable\",\n        },\n        {\n    trigger: '.o_field_widget input.o_input',\n    content: markup(_t('Enter the <b>subject</b> of your ticket <br/><i>(e.g. Problem with my installation, Wrong order, etc.).</i>')),\n    tooltipPosition: 'right',\n            run: \"edit SAP is bad, paid by Odoo\",\n        },\n        {\n            trigger: \".o_form_editable\",\n        },\n        {\n    isActive: [\"manual\"],\n    trigger: \".o_field_widget[name='partner_id']\",\n    content: markup(_t('Select the <b>customer</b> of your ticket.')),\n    tooltipPosition: \"top\",\n    run: \"click\",\n        },\n        {\n    isActive: [\"auto\"],\n    trigger: 'div[name=partner_id] input',\n    content: markup(_t('Select the <b>customer</b> of your ticket.')),\n    tooltipPosition: 'top',\n            run: \"edit Admin\",\n        },\n        {\n            trigger: \".o_form_editable\",\n        },\n        {\n            isActive: [\"auto\"],\n            trigger: \".ui-autocomplete > li > a:not(:has(i.fa))\",\n            run: \"click\",\n            tooltipPosition: \"right\",\n        },\n        {\n            trigger: \".o_form_view\",\n        },\n        {\n    trigger: '.o_kanban_quick_create button.o_kanban_add',\n    content: _t('Save this ticket and the modifications you\\'ve made to it.'),\n    tooltipPosition: 'bottom',\n            run: \"click\",\n        },\n        {\n            trigger: \".o_form_view\",\n        },\n        {\n    trigger: '.o_kanban_record',\n    content: _t('Open the ticket.'),\n    tooltipPosition: 'bottom',\n            run: \"click\",\n        },\n        {\n            trigger: \".o_form_view\",\n        },\n        {\n    trigger: \".o-mail-Chatter-topbar button.o-mail-Chatter-sendMessage\",\n    content: markup(_t(\"Use the chatter to <b>send emails</b> and communicate efficiently with your customers. Add new people to the followers' list to make them aware of the progress of this ticket.\")),\n    tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_form_view\",\n        },\n        {\n    trigger: \"button.o-mail-Chatter-logNote\",\n    content: markup(_t(\"<b>Log notes</b> for internal communications (you will only notify the persons you specifically tag). Use <b>@ mentions</b> to ping a colleague or <b># mentions</b> to contact a group of people.\")),\n    tooltipPosition: \"bottom\",\n            run: \"click\",\n        },\n        {\n            trigger: \".o_form_view .o_form_saved\",\n        },\n        {\n    trigger: \"button.o-mail-Chatter-activity\",\n    content: markup(_t(\"Use <b>activities</b> to organize your daily work.\")),\n    run: \"click\",\n        },\n        {\n    trigger: \".modal-dialog .btn-primary\",\n    content: markup(_t(\"Schedule your <b>activity</b>.\")),\n    tooltipPosition: \"right\",\n    run: \"click\",\n        },\n        {\n            trigger: \".o_form_view\",\n        },\n        {\n    trigger: '.o_back_button',\n    content: markup(_t(\"Let's go back to the <b>kanban view</b> to get an overview of your next tickets.\")),\n    tooltipPosition: 'bottom',\n    run: \"click\",\n        },\n        {\n    trigger: 'body:not(:has(div.o_view_sample_data)) .o_helpdesk_ticket_kanban_view .o_kanban_record',\n    content: markup(_t('<b>Drag &amp; drop</b> the card to change the stage of your ticket.')),\n    tooltipPosition: 'right',\n            run: \"drag_and_drop(.o_kanban_group:eq(2))\",\n        },\n        {\n    trigger: \".o_column_quick_create .o_quick_create_folded\",\n    content: markup(_t('Adapt your <b>pipeline</b> to your workflow by adding <b>stages</b> <i>(e.g. Awaiting Customer Feedback, etc.).</i>')),\n    tooltipPosition: 'bottom',\n    run: \"click\",\n        },\n        {\n    trigger: \".o_column_quick_create .o_kanban_add\",\n    content: _t(\"Add your stage and place it at the right step of your workflow by dragging & dropping it.\"),\n    tooltipPosition: 'bottom',\n    run: \"click\",\n        },\n        {\n            trigger: \".o_column_quick_create .o_kanban_add\",\n            content:\n                \"Clicking on 'Add' when input name is empty won't do anything, 'Add' will still be displayed\",\n        },\n    ],\n});\n", "/** @odoo-module **/\n\nexport class BarcodeObject {\n    constructor(rawValue) {\n        this.rawValue = rawValue; // Untouched barcode.\n\n        this.nomenclature = undefined; // Nomenclature this barcode uses.\n        this.parsedBarcode = undefined; // Barcode's value(s) once it has been parsed.\n        this.parsedData = {}; // Fetched data thanks to the barcode parsed value(s).\n        this.isParsed = false; // Flag to know if barcode was already parsed.\n        this.missingRecords = []; // Keep track of missing records (try to fetch them later.)\n        this.isURN = Boolean(this.rawValue.match(/^urn:.*$/));\n\n        if (this.parser) {\n            try {\n                this.parsedBarcode = this.parser.parse_barcode(this.rawValue);\n            } catch (err) {\n                // The barcode can't be parsed but the error is caught to fallback\n                // on the classic way to handle barcodes.\n                console.log(`%cWarning: error about ${this.rawValue}`, 'text-weight: bold;');\n                console.log(err.message);\n            }\n            if (this.parsedBarcode && !Array.isArray(this.parsedBarcode)) {\n                // Depending of the nomenclature, the parsed data is either an object,\n                // either an array of objects. Convert it into an array in all case.\n                this.parsedBarcode = [this.parsedBarcode];\n            }\n            this.isParsed = Boolean(this.parsedBarcode?.length);\n        } else {\n            console.warn(\"No parser set !\");\n        }\n\n        // Adds this instance into the raw barcode/barcode object mapping.\n        BarcodeObject.mappingRawBarcodeToObject[rawValue] = this;\n    }\n\n    /**\n     * Attach to the barcode record(s) already in the cache.\n     * For missing record(s), they need to be fetched afterward.\n     */\n    async setRecords(options=false) {\n        if (!this.isParsed) {\n            return;\n        }\n        options = options || {\n            fetchLater: true,\n            onlyInCache: true,\n        };\n        this.missingRecords = [];\n        for (const barcodeData of this.parsedBarcode) {\n            const { type, code } = barcodeData;\n            if (type === \"product\") {\n                await this.fetchProduct(code, options);\n            } else if (type === \"lot\") {\n                await this.fetchTrackingNumber(code, options);\n            }\n        }\n    }\n\n    // Getters\n    get cache() {\n        return BarcodeObject.__cache;\n    }\n\n    get hasMissingRecords() {\n        return this.isParsed && Boolean(this.missingRecords.length);\n    }\n\n    get parser() {\n        return BarcodeObject.__parser;\n    }\n\n    // Fetching methods\n    async fetchTrackingNumber(lotBarcode, options) {\n        const lot = await this.cache.getRecordByBarcode(lotBarcode, \"stock.lot\", options);\n        if (lot) {\n            this.parsedData.lot = lot;\n        } else {\n            this.missingRecords.push({ type: \"lot\", lotBarcode });\n        }\n    }\n\n    async fetchProduct(productBarcode, options) {\n        let product = await this.cache.getRecordByBarcode(productBarcode, \"product.product\", options);\n        if (!product) {\n            const packaging = await this.cache.getRecordByBarcode(productBarcode, \"product.packaging\", {\n                onlyInCache: true,\n            });\n            if (packaging) {\n                product = this.cache.getRecord(\"product.product\", packaging.product_id, false);\n                this.parsedData.packaging = packaging;\n                this.parsedData.quantity = packaging.qty;\n            }\n        }\n        if (product) {\n            this.parsedData.product = product;\n        } else {\n            this.missingRecords.push({ type: \"product\", productBarcode });\n        }\n    }\n}\n\n// Static properties and methods.\nBarcodeObject.mappingRawBarcodeToObject = {};\n\nBarcodeObject.setEnv = (cache, parser) => {\n    BarcodeObject.__cache = cache;\n    BarcodeObject.__parser = parser;\n}\n\nBarcodeObject.forBarcode = (barcode) => {\n    if (BarcodeObject.mappingRawBarcodeToObject[barcode]) {\n        return BarcodeObject.mappingRawBarcodeToObject[barcode];\n    }\n    return new BarcodeObject(barcode);\n};\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { Component } from \"@odoo/owl\";\n\nexport class ApplyQuantDialog extends Component {\n    static components = { Dialog };\n    static props = {\n        onApply: Function,\n        onApplyAll: Function,\n        close: Function,\n    };\n    static template = \"stock_barcode.ApplyQuantDialog\";\n\n    onApply() {\n        this.props.onApply();\n        this.props.close();\n    }\n\n    onApplyAll() {\n        this.props.onApplyAll();\n        this.props.close();\n    }\n}\n", "import { Dialog } from \"@web/core/dialog/dialog\";\nimport { Component } from \"@odoo/owl\";\n\nexport class BackorderDialog extends Component {\n    static components = { Dialog };\n    static props = {\n        displayUoM: Boolean,\n        uncompletedLines: Array,\n        onApply: Function,\n        close: Function,\n    };\n    static template = \"stock_barcode.BackorderDialog\";\n\n    async _onApply() {\n        await this.props.onApply();\n        this.props.close();\n    }\n}\n", "/** @odoo-module **/\n\nimport { Component, onWillUnmount, onWillUpdateProps, useState } from \"@odoo/owl\";\nimport { session } from \"@web/session\";\n\nexport class CountScreenRFID extends Component {\n    static props = {\n        close: Function,\n        receivedRFIDs: { type: Array, default: [] },\n        totalRFIDs: { type: Array, default: [] },\n    };\n    static template = \"stock_barcode.CountScreenRFID\";\n\n    setup() {\n        this.state = useState({\n            duration: \"00:00\",\n            readRate: 0,\n        });\n        this.delayBeforeStopRefreshRate = (session.time_between_reads_in_ms || 100) * 2;\n        this.totalSeconds = 0;\n        this.activeScanningTotalTime = 0;\n        this.setActiveScanning();\n        this.initialTimestamp = Date.now();\n        this.timeInterval = setInterval(() => {\n            const currentTimestamp = Date.now();\n            const milliseconds = currentTimestamp - this.initialTimestamp;\n            this.totalSeconds = Math.floor(milliseconds / 1000);\n            const seconds = this.totalSeconds % 60;\n            const minutes = Math.floor(this.totalSeconds / 60);\n            const strSeconds = String(seconds).padStart(2, \"0\");\n            const strMinutes = String(minutes).padStart(2, \"0\");\n            this.state.duration = `${strMinutes}:${strSeconds}`;\n        }, 1000);\n        this.updateReadsRateInterval = setInterval(() => {\n            if (this.activeScanning) {\n                this.activeScanningTotalTime += 50;\n            }\n            const seconds = this.activeScanningTotalTime / 1000;\n            const divider = Math.max(seconds, 1);\n            this.state.readRate = Math.floor(this.props.receivedRFIDs.length / divider);\n        }, 50);\n\n        onWillUpdateProps(() => {\n            clearTimeout(this.activeScanningTimeout);\n            this.setActiveScanning();\n        });\n\n        onWillUnmount(() => {\n            clearInterval(this.updateReadsRateInterval);\n            clearTimeout(this.activeScanningTimeout);\n        });\n    }\n\n    setActiveScanning () {\n        if (this.activeScanningTimeout) {\n            clearTimeout(this.activeScanningTimeout);\n        }\n        this.activeScanning = true;\n        this.activeScanningTimeout = setTimeout(() => {\n            this.activeScanning = false;\n        }, this.delayBeforeStopRefreshRate);\n    }\n\n    get totalRead() {\n        return this.props.totalRFIDs.length;\n    }\n\n    get uniqueTags() {\n        return new Set(this.props.totalRFIDs).size;\n    }\n}\n", "/** @odoo-module **/\n\nimport LineComponent from \"@stock_barcode/components/line\";\n\nexport default class GroupedLineComponent extends LineComponent {\n    static components = { LineComponent };\n    static template = \"stock_barcode.GroupedLineComponent\";\n\n    get isComplete() {\n        if (this.linesToDisplay.length > 1 && this.isTracked &&\n            this.qtyDemand && this.qtyDone === this.qtyDemand) {\n            // In case the line is tracked and has multiple sublines, we consider the line complete\n            // if it has enough quantity and all sublines with quantity has a lot.\n            for (const subline of this.linesToDisplay) {\n                const lotName = subline.lot_id?.name || subline.lot_name;\n                if (this.env.model.getQtyDone(subline) && !lotName) {\n                    return false;\n                }\n            }\n            return true;\n        }\n        return super.isComplete;\n    }\n\n    get isSelected() {\n        return this.line.virtual_ids.indexOf(this.env.model.selectedLineVirtualId) !== -1;\n    }\n\n    get opened() {\n        return this.env.model.groupKey(this.line) === this.env.model.unfoldLineKey;\n    }\n\n    get sublineProps() {\n        return {\n            displayUOM: this.props.displayUOM,\n            editLine: this.props.editLine,\n            line: this.subline,\n            subline: true,\n        };\n    }\n\n    get linesToDisplay() {\n        if (!this.env.model.showReservedSns) {\n            return this.props.line.lines.filter(line => {\n                return this.env.model.getQtyDone(line) > 0 || line.product_id.tracking == \"none\" || this.env.model.getQtyDemand(line) == 0;\n            });\n        }\n        return this.props.line.lines;\n    }\n\n    get lotName() {\n        if (!this.env.model.showReservedSns) {\n            // In case we don't display unscanned reserved lots, display it only\n            // if only one subline with a lot has some quantity done.\n            if (this.linesToDisplay.length === 1) {\n                for (const line of this.linesToDisplay) {\n                    const lotName = line.lot_id?.name || line.lot_name;\n                    if (lotName && this.env.model.getQtyDone(this.line)) {\n                        return lotName;\n                    }\n                }\n            } else {\n                return \"\";\n            }\n        }\n        return super.lotName;\n    }\n\n    get displayToggleBtn() {\n        return this.linesToDisplay.length > 1;\n    }\n\n    toggleSublines(ev) {\n        ev.stopPropagation();\n        this.env.model.toggleSublines(this.line);\n    }\n}\n", "/** @odoo-module **/\n\nimport { Component } from \"@odoo/owl\";\nimport { ProductImageDialog } from '@stock_barcode/components/product_image_dialog';\n\nexport default class LineComponent extends Component {\n    static props = [\"displayUOM\", \"line\", \"subline?\", \"editLine\"];\n    static template = \"stock_barcode.LineComponent\";\n\n    setup() {\n        this.imageSource = this.props.line.product_id.has_image\n            ? `/web/image/product.product/${this.props.line.product_id.id}/image_128`\n            : null;\n    }\n\n    get destinationLocationPath () {\n        return this._getLocationPath(this.env.model._defaultDestLocation(), this.line.location_dest_id);\n    }\n\n    get displayDeleteButton() {\n        return this.env.model.lineCanBeDeleted(this.line);\n    }\n\n    get displayDestinationLocation() {\n        return !this.props.subline && this.env.model.displayDestinationLocation;\n    }\n\n    get displayFulfillbutton() {\n        return this.incrementQty && this.env.model.getDisplayIncrementBtn(this.line);\n    }\n\n    get displayIncrementButton() {\n        if (this.isSelected && this.incrementQty !== 1) {\n            return this.isTracked && this.line.product_id.tracking === \"serial\"\n                ? this.env.model.getDisplayIncrementBtnForSerial(this.line)\n                : this.env.model.getDisplayIncrementBtn(this.line);\n        }\n        return false;\n    }\n\n    get incrementQty() {\n        return this.env.model.getIncrementQuantity(this.line);\n    }\n\n    get displayResultPackage() {\n        return this.env.model.displayResultPackage;\n    }\n\n    get isComplete() {\n        if (!this.qtyDemand || this.qtyDemand != this.qtyDone) {\n            return false;\n        } else if (this.isTracked && !this.lotName) {\n            return false;\n        }\n        return true;\n    }\n\n    get isSelected() {\n        return this.env.model.lineIsSelected(this.line) ||\n        (this.line.package_id && this.line.package_id.id === this.env.model.lastScanned.packageId);\n    }\n\n    get isTracked() {\n        return this.env.model.lineIsTracked(this.line);\n    }\n\n    get lotName() {\n        if (this.env.model.showReservedSns || this.env.model.getQtyDone(this.line)) {\n            return (this.line.lot_id && this.line.lot_id.name) || this.line.lot_name || \"\";\n        }\n        return \"\";\n    }\n\n    get nextExpected() {\n        if (!this.isSelected) {\n            return false;\n        } else if (this.isTracked && !this.lotName) {\n            return 'lot';\n        } else if (this.qtyDemand && this.qtyDone < this.qtyDemand) {\n            return 'quantity';\n        }\n    }\n\n    get qtyDemand() {\n        return this.env.model.getQtyDemand(this.line);\n    }\n\n    get qtyDone() {\n        return this.env.model.getQtyDone(this.line);\n    }\n\n    get quantityIsSet() {\n        return this.line.inventory_quantity_set;\n    }\n\n    get line() {\n        return this.props.line;\n    }\n\n    get componentClasses() {\n        return [\n            this.isComplete ? 'o_line_completed' : 'o_line_not_completed',\n            this.env.model.lineIsFaulty(this.line) ? 'o_faulty' : '',\n            this.isSelected ? 'o_selected o_highlight' : ''\n        ].join(' ');\n    }\n\n    _getLocationPath(rootLocation, currentLocation) {\n        let locationName = currentLocation.display_name;\n        if (this.env.model.shouldShortenLocationName && this.env.model._isSublocation &&\n            this.env.model._isSublocation(currentLocation, rootLocation) &&\n            rootLocation && rootLocation.id != currentLocation.id) {\n            locationName = locationName.replace(rootLocation.display_name, '...');\n        }\n        return locationName.replace(new RegExp(currentLocation.name + '$'), '');\n    }\n\n    addQuantity(quantity) {\n        this.env.model.updateLineQty(this.line.virtual_id, quantity);\n    }\n\n    select(ev) {\n        ev.stopPropagation();\n        this.env.model.selectLine(this.line);\n        this.env.model.trigger('update');\n    }\n\n    toggleAsCounted(ev) {\n        this.env.model.toggleAsCounted(this.line);\n    }\n\n    onClickImage() {\n        this.env.dialog.add(ProductImageDialog, { record: this.line.product_id });\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Chatter } from \"@mail/chatter/web_portal/chatter\";\nimport { COMMANDS } from \"@barcodes/barcode_handlers\";\nimport BarcodePickingModel from '@stock_barcode/models/barcode_picking_model';\nimport BarcodeQuantModel from '@stock_barcode/models/barcode_quant_model';\nimport GroupedLineComponent from '@stock_barcode/components/grouped_line';\nimport LineComponent from '@stock_barcode/components/line';\nimport PackageLineComponent from '@stock_barcode/components/package_line';\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { useService, useBus } from \"@web/core/utils/hooks\";\nimport { Mutex } from \"@web/core/utils/concurrency\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { View } from \"@web/views/view\";\nimport { BarcodeVideoScanner, isBarcodeScannerSupported } from '@web/core/barcode/barcode_video_scanner';\nimport { url } from '@web/core/utils/urls';\nimport { utils as uiUtils } from \"@web/core/ui/ui_service\";\nimport { Component, EventBus, onPatched, onWillStart, onWillUnmount, useState, useSubEnv } from \"@odoo/owl\";\nimport { ImportBlockUI } from \"@base_import/import_block_ui\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\nimport { BarcodeInput } from \"./manual_barcode\";\nimport { CountScreenRFID } from \"./count_screen_rfid\";\n\n// Lets `barcodeGenericHandlers` knows those commands exist so it doesn't warn when scanned.\nCOMMANDS[\"OCDMENU\"] = () => {};\nCOMMANDS[\"OCDCANC\"] = () => {};\n\nconst bus = new EventBus();\n\nclass StockBarcodeUnlinkButton extends Component {\n    static template = \"stock_barcode.UnlinkButton\";\n    static props = {...standardWidgetProps};\n    setup() {\n        this.orm = useService(\"orm\");\n    }\n    async onClick() {\n        const { resModel, resId, context } = this.props.record;\n        await this.orm.unlink(resModel, [resId], { context });\n        bus.trigger(\"refresh\");\n    }\n}\nregistry.category(\"view_widgets\").add(\"stock_barcode_unlink_button\", {\n    component: StockBarcodeUnlinkButton,\n});\n\n/**\n * Main Component\n * Gather the line information.\n * Manage the scan and save process.\n */\n\nclass MainComponent extends Component {\n    static props = { ...standardActionServiceProps };\n    static template = \"stock_barcode.MainComponent\";\n    static components = {\n        BarcodeInput,\n        BarcodeVideoScanner,\n        Chatter,\n        CountScreenRFID,\n        GroupedLineComponent,\n        ImportBlockUI,\n        LineComponent,\n        PackageLineComponent,\n        View,\n    };\n\n    //--------------------------------------------------------------------------\n    // Lifecycle\n    //--------------------------------------------------------------------------\n\n    setup() {\n        this.orm = useService('orm');\n        this.notification = useService('notification');\n        this.dialog = useService('dialog');\n        this.action = useService('action');\n        this.actionMutex = new Mutex();\n        this.resModel = this.props.action.res_model;\n        this.resId = this.props.action.context.active_id || false;\n        const model = this._getModel();\n        model.newScrapProduct = this.newScrapProduct.bind(this);\n        useSubEnv({\n            model,\n            dialog: this.dialog,\n        });\n        this._scrollBehavior = 'smooth';\n        this.isMobile = uiUtils.isSmall();\n        this.state = useState({\n            cameraScannedEnabled: false,\n            view: \"barcodeLines\", // Could be also 'printMenu' or 'editFormView'.\n            displayNote: false,\n            displayCountRFID: false,\n            uiBlocked: false,\n            barcodesProcessed: 0,\n            barcodesToProcess: 0,\n            readyToToggleCamera: true,\n        });\n        this.bufferedBarcodes = [];\n        this.receivedRFIDs = [];\n        this.totalRFIDs = [];\n        this.bufferingTimeout = null;\n        this.barcodeService = useService(\"barcode\");\n        useBus(this.barcodeService.bus, \"barcode_scanned\", (ev) =>\n            this.onBarcodeScanned(ev.detail.barcode)\n        );\n        this.mobileService = useService(\"mobile\");\n        useBus(this.mobileService.bus, \"mobile_reader_scanned\", (ev) =>\n            this.onMobileReaderScanned(ev.detail.data)\n        );\n\n        useBus(this.env.model, 'flash', this.flashScreen.bind(this));\n        useBus(this.env.model, \"playSound\", this.playSound.bind(this));\n        useBus(this.env.model, \"blockUI\", this.blockUI.bind(this));\n        useBus(this.env.model, \"unblockUI\", this.unblockUI.bind(this));\n        useBus(this.env.model, \"addBarcodesCountToProcess\", (ev) => this.addBarcodesCountToProcess(ev.detail));\n        useBus(this.env.model, \"updateBarcodesCountProcessed\", this.updateBarcodesCountProcessed.bind(this));\n        useBus(this.env.model, \"clearBarcodesCountProcessed\", this.clearBarcodesCountProcessed.bind(this));\n        useBus(bus, \"refresh\", (ev) => this._onRefreshState(ev.detail));\n\n        onWillStart(() => this.onWillStart());\n\n        onWillUnmount(() => {\n            clearTimeout(this.bufferingTimeout);\n        });\n\n        onPatched(() => {\n            this._scrollToSelectedLine();\n        });\n\n        onWillUnmount(() => {\n            this.env.model._onExit();\n        });\n    }\n\n    // UI Methods --------------------------------------------------------------\n    addBarcodesCountToProcess(count) {\n        this.state.barcodesToProcess += count;\n        if (this.state.barcodesToProcess > this.state.barcodesProcessed) {\n            this.updateBarcodesCountMessage();\n            this.blockUI();\n        }\n    }\n\n    updateBarcodesCountProcessed() {\n        this.state.barcodesProcessed++;\n        this.updateBarcodesCountMessage();\n        if (this.state.barcodesProcessed >= this.state.barcodesToProcess) {\n            this.clearBarcodesCountProcessed();\n        }\n    }\n\n    clearBarcodesCountProcessed() {\n        this.state.barcodesProcessed = 0;\n        this.state.barcodesToProcess = 0;\n        this.unblockUI();\n    }\n\n    updateBarcodesCountMessage() {\n        this.blockUIMessage = _t(\"Processing %(processed)s/%(toProcess)s barcodes\", {\n            processed: this.state.barcodesProcessed,\n            toProcess: this.state.barcodesToProcess,\n        });\n    }\n\n    blockUI(ev) {\n        this.state.uiBlocked = true;\n    }\n\n    unblockUI() {\n        this.state.uiBlocked = false;\n        this.render(true);\n    }\n\n    playSound(ev) {\n        if (!this.config.play_sound || this.state.uiBlocked) {\n            return;\n        }\n        const type = ev.detail || \"notify\";\n        this.sounds[type].currentTime = 0;\n        this.sounds[type].play().catch((error) => {\n            // `play` returns a promise. In case this promise is rejected (permission\n            // issue for example), catch it to avoid Odoo's `UncaughtPromiseError`.\n            console.log(error);\n        });\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    get highlightValidateButton() {\n        return this.env.model.highlightValidateButton;\n    }\n\n    async onWillStart() {\n        const barcodeData = await rpc(\"/stock_barcode/get_barcode_data\", {\n            model: this.resModel,\n            res_id: this.resId,\n        });\n        barcodeData.actionId = this.props.actionId;\n        this.config = { play_sound: true, ...barcodeData.data.config };\n        if (this.config.play_sound) {\n            const fileExtension = new Audio().canPlayType(\"audio/ogg\") ? \"ogg\" : \"mp3\";\n            this.sounds = {\n                error: new Audio(url(`/barcodes/static/src/audio/error.${fileExtension}`)),\n                notify: new Audio(url(`/mail/static/src/audio/ting.${fileExtension}`)),\n                success: new Audio(url(`/stock_barcode/static/src/audio/success.${fileExtension}`)),\n            };\n            this.sounds.error.load();\n            this.sounds.notify.load();\n            this.sounds.success.load();\n        }\n        this.setupCameraScanner();\n        this.groups = barcodeData.groups;\n        this.env.model.setData(barcodeData);\n        this.state.displayNote = Boolean(this.env.model.record.note);\n        this.env.model.addEventListener(\"process-action\", this._onDoAction.bind(this));\n        this.env.model.addEventListener(\"refresh\", (ev) => this._onRefreshState(ev.detail));\n        this.env.model.addEventListener(\"update\", () => {\n            if (!this.state.uiBlocked) {\n                this.render(true);\n            }\n        });\n        this.env.model.addEventListener(\"history-back\", () => this._exit());\n    }\n\n    get isTransfer() {\n        return this.currentSourceLocation && this.currentDestinationLocation;\n    }\n\n    get lineFormViewProps() {\n        return {\n            resId: this._editedLineParams && this._editedLineParams.currentId,\n            resModel: this.env.model.lineModel,\n            context: this.env.model._getNewLineDefaultContext(),\n            viewId: this.env.model.lineFormViewId,\n            display: { controlPanel: false },\n            mode: \"edit\",\n            type: \"form\",\n            onSave: (record) => this.saveFormView(record),\n            onDiscard: () => this.toggleBarcodeLines(),\n        };\n    }\n\n    get lines() {\n        return this.env.model.groupedLines;\n    }\n\n    get packageLines() {\n        return this.env.model.packageLines;\n    }\n\n    get addLineBtnName() {\n        return _t('Add Product');\n    }\n\n    get displayActionButtons() {\n        return this.state.view === 'barcodeLines' && this.env.model.canBeProcessed;\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    _getModel() {\n        const services = { rpc: rpc, orm: this.orm, notification: this.notification, action: this.action };\n        if (this.resModel === 'stock.picking') {\n            services.dialog = this.dialog;\n            return new BarcodePickingModel(this.resModel, this.resId, services);\n        } else if (this.resModel === 'stock.quant') {\n            return new BarcodeQuantModel(this.resModel, this.resId, services);\n        } else {\n            throw new Error('No JS model define');\n        }\n    }\n\n    //--------------------------------------------------------------------------\n    // Camera scanner\n    //--------------------------------------------------------------------------\n\n    toggleCameraScanner() {\n        if (!this.state.cameraScannedEnabled) {\n            this.state.cameraScannedEnabled = true;\n            this.state.readyToToggleCamera = false;\n        } else if (this.state.readyToToggleCamera) {\n            this.state.cameraScannedEnabled = false;\n        }\n    }\n\n    setupCameraScanner() {\n        this.cameraScannerSupported = isBarcodeScannerSupported();\n        this.barcodeVideoScannerProps = {\n            delayBetweenScan: this.config.delay_between_scan || 2000,\n            facingMode: \"environment\",\n            onResult: (barcode) => this.onBarcodeScanned(barcode),\n            onError: (error) => {\n                this.state.cameraScannedEnabled = false;\n                const message = error.message;\n                this.notification.add(message, { type: 'warning' });\n            },\n            onReady: () => {\n                this.state.readyToToggleCamera = true;\n            },\n            cssClass: \"o_stock_barcode_camera_video\",\n        };\n    }\n\n    get cameraScannerClassState() {\n        if (!this.state.readyToToggleCamera) {\n            return \"bg-secondary\";\n        }\n        return this.state.cameraScannedEnabled ? \"bg-success text-white\" : \"text-primary\";\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    async cancel() {\n        await this.env.model.save();\n        const action = await this.orm.call(\n            this.resModel,\n            'action_cancel_from_barcode',\n            [[this.resId]]\n        );\n        const onClose = res => {\n            if (res && res.cancelled) {\n                this.env.model._cancelNotification();\n                this._exit();\n            }\n        };\n        this.action.doAction(action, {\n            onClose: onClose.bind(this),\n        });\n    }\n\n    onBarcodeScanned(barcode) {\n        if (barcode) {\n            this.actionMutex.exec(async () => {\n                return this.env.model.processBarcode(barcode);\n            });\n            if ('vibrate' in window.navigator) {\n                window.navigator.vibrate(100);\n            }\n        } else {\n            const message = _t(\"Please, Scan again!\");\n            this.env.services.notification.add(message, { type: 'warning' });\n        }\n    }\n\n    onMobileReaderScanned(data) {\n        this.receivedRFIDs.push(...data);\n        this.totalRFIDs.push(...data);\n        this.state.displayCountRFID = true;\n        if (this.RFIDCountTimeout) {\n            clearTimeout(this.RFIDCountTimeout);\n        }\n        this.RFIDCountTimeout = setTimeout(() => this.closeRFIDCount(), 5000);\n        if (!this.bufferingTimeout) {\n            this.bufferingTimeout = setTimeout(\n                this._onMobileReaderScanned.bind(this),\n                this.config.barcode_rfid_batch_time\n            );\n        }\n        this.bufferedBarcodes = this.bufferedBarcodes.concat(data);\n    }\n\n    async _onMobileReaderScanned(ev) {\n        await this.env.model.processBarcode(this.bufferedBarcodes.join(\",\"), { readingRFID: true });\n        this.bufferedBarcodes = [];\n        clearTimeout(this.bufferingTimeout);\n        this.bufferingTimeout = null;\n    }\n\n    closeRFIDCount() {\n        if (this.RFIDCountTimeout) {\n            clearTimeout(this.RFIDCountTimeout);\n        }\n        this.state.displayCountRFID = false;\n        this.receivedRFIDs = [];\n    }\n\n    onBarcodeSubmitted(barcode) {\n        this.changeView(\"barcodeLines\");\n        barcode = this.env.model.cleanBarcode(barcode);\n        this.onBarcodeScanned(barcode);\n    }\n\n    async exit(ev) {\n        this.state.cameraScannedEnabled = false;\n        if (this.state.view === \"barcodeLines\") {\n            await this.env.model.beforeQuit();\n            this._exit();\n        } else {\n            this.toggleBarcodeLines();\n        }\n    }\n\n    _exit() {\n        const { breadcrumbs } = this.env.config;\n        if (breadcrumbs.length === 1) {\n            // Bring back to the Barcode App home menu when there is no breadcrumb.\n            this.action.doAction(\"stock_barcode.stock_barcode_action_main_menu\");\n        } else {\n            const previousPath = breadcrumbs[breadcrumbs.length - 2].url.split(\"/\");\n\n            if (isNaN(previousPath[previousPath.length - 1])) {\n                this.env.config.historyBack();\n            } else {\n                // If previous controller path's last part is a number, it will\n                // open the current record form view (happens after a refresh of\n                // the web browser.) Avoid that by calling browser's history back.\n                history.back();\n            }\n        }\n    }\n\n    flashScreen() {\n        if (this.state.uiBlocked) {\n            return;\n        }\n        const clientAction = document.querySelector('.o_barcode_client_action');\n        // Resets the animation (in case it still going).\n        clientAction.style.animation = 'none';\n        clientAction.offsetHeight; // Trigger reflow.\n        clientAction.style.animation = null;\n        // Adds the CSS class linked to the keyframes animation `white-flash`.\n        clientAction.classList.add('o_white_flash');\n    }\n\n    putInPack(ev) {\n        ev.stopPropagation();\n        this.env.model._putInPack();\n    }\n\n    returnProducts(ev){\n        ev.stopPropagation();\n        this.env.model._returnProducts();\n    }\n\n    saveFormView(lineRecord) {\n        const lineId = (lineRecord && lineRecord.resId) || (this._editedLineParams && this._editedLineParams.currentId);\n        const recordId = (lineRecord.resModel === this.resModel) ? lineId : undefined;\n        this._onRefreshState({ recordId, lineId });\n    }\n\n    changeView(view) {\n        this.state.cameraScannedEnabled = false;\n        this.state.view = view;\n    }\n\n    async toggleBarcodeLines(lineId) {\n        await this.env.model.displayBarcodeLines(lineId);\n        this._editedLineParams = undefined;\n        this.changeView(\"barcodeLines\");\n    }\n\n    async toggleInformation() {\n        if (this.env.model.formViewId) {\n            if (this.state.view === \"infoFormView\") {\n                this.changeView(\"barcodeLines\");\n            } else {\n                await this.env.model.save();\n                this.changeView(\"infoFormView\");\n            }\n        }\n    }\n\n    /**\n     * Calls `validate` on the model and then triggers up the action because OWL\n     * components don't seem able to manage wizard without doing custom things.\n     *\n     * @param {OdooEvent} ev\n     */\n    async validate(ev) {\n        ev.stopPropagation();\n        await this.env.model.validate();\n    }\n\n    _getHeaderHeight() {\n        const header = document.querySelector('.o_barcode_header');\n        const navbar = document.querySelector('.o_main_navbar');\n        // Computes the real header's height (the navbar is present if the page was refreshed).\n        return navbar ? navbar.offsetHeight + header.offsetHeight : header.offsetHeight;\n    }\n\n    _scrollToSelectedLine() {\n        if (!this.state.view === \"barcodeLines\" && this.env.model.canBeProcessed) {\n            this._scrollBehavior = 'auto';\n            return;\n        }\n        // Tries to scroll to selected subline.\n        let targetElement = false;\n        let selectedLine = document.querySelector('.o_sublines .o_barcode_line.o_highlight');\n        const isSubline = Boolean(selectedLine);\n        // If no selected subline, tries to scroll to selected line.\n        if (!selectedLine) {\n            selectedLine = document.querySelector('.o_barcode_line.o_highlight');\n        }\n\n        let locationLine = false;\n        if (this.env.model.lastScanned.sourceLocation) {\n            const locId = this.env.model.lastScanned.sourceLocation.id;\n            locationLine = document.querySelector(`.o_barcode_location_line[data-location-id=\"${locId}\"]`);\n        } else if (selectedLine) {\n            locationLine = selectedLine.closest('.o_barcode_location_group').querySelector(\".o_barcode_location_line\");\n        }\n        // Scrolls either to the selected line, either to the location line.\n        targetElement = selectedLine || (locationLine && locationLine.parentElement);\n\n        if (targetElement) {\n            // If a line is selected, checks if this line is on the top of the\n            // page, and if it's not, scrolls until the line is on top.\n            const elRect = targetElement.getBoundingClientRect();\n            const page = document.querySelector('.o_barcode_lines');\n            const headerHeight = this._getHeaderHeight();\n            if (elRect.top < headerHeight || elRect.bottom > (headerHeight + elRect.height)) {\n                let top = elRect.top - headerHeight + page.scrollTop;\n                if (isSubline) {\n                    const parentLine = targetElement.closest('.o_sublines').closest('.o_barcode_line');\n                    const parentSummary = parentLine.querySelector('.o_barcode_line_summary');\n                    top -= parentSummary.getBoundingClientRect().height;\n                } else if (selectedLine && locationLine) {\n                    top -= locationLine.getBoundingClientRect().height;\n                }\n                page.scroll({ left: 0, top, behavior: this._scrollBehavior });\n                this._scrollBehavior = 'smooth';\n            }\n\n        }\n    }\n\n    async _onDoAction(ev) {\n        this.action.doAction(ev.detail, {\n            onClose: this._onRefreshState.bind(this),\n        });\n    }\n\n    onOpenPackage(packageId) {\n        this._inspectedPackageId = packageId;\n        this.changeView(\"packagePage\");\n    }\n\n    async newScrapProduct() {\n        await this.env.model.save();\n        this.changeView(\"scrapProductPage\");\n    }\n\n    get displayOperationButtons() {\n        const { model } = this.env;\n        return model.canScrap || model.displayCancelButton || model.displaySignatureButton || model.displayReturnButton;\n    }\n\n    get scrapViewProps() {\n        const context = this.env.model.scrapContext;\n        return {\n            resModel: 'stock.scrap',\n            context: context,\n            viewId: this.env.model.scrapViewId,\n            display: { controlPanel: false },\n            mode: \"edit\",\n            type: \"form\",\n            onSave: () => this.toggleBarcodeLines(),\n            onDiscard: () => this.toggleBarcodeLines(),\n        };\n    }\n\n    async onOpenProductPage(line) {\n        await this.env.model.save();\n        if (line) {\n            const virtualId = line.virtual_id;\n            // Updates the line id if it's missing, in order to open the line form view.\n            if (!line.id && virtualId) {\n                line = this.env.model.pageLines.find(l => l.dummy_id === virtualId);\n            }\n            this._editedLineParams = this.env.model.getEditedLineParams(line);\n        }\n        this.changeView(\"productPage\");\n    }\n\n    async _onRefreshState(paramsRefresh) {\n        const { recordId, lineId } = paramsRefresh || {}\n        const { route, params } = this.env.model.getActionRefresh(recordId);\n        const result = await rpc(route, params);\n        await this.env.model.refreshCache(result.data.records);\n        await this.toggleBarcodeLines(lineId);\n        this.render();\n    }\n\n    /**\n     * Handles triggered warnings. It can happen from an onchange for example.\n     *\n     * @param {CustomEvent} ev\n     */\n    _onWarning(ev) {\n        const { title, message } = ev.detail;\n        this.env.services.dialog.add(ConfirmationDialog, { title, body: message });\n    }\n}\n\nregistry.category(\"actions\").add(\"stock_barcode_client_action\", MainComponent);\n\nexport default MainComponent;\n", "import { BarcodeDialog } from '@web/core/barcode/barcode_dialog';\nimport { Component, onMounted, useRef, useState } from \"@odoo/owl\";\n\nexport class BarcodeInput extends Component {\n    static template = \"stock_barcode.BarcodeInput\";\n    static props = {\n        onSubmit: Function,\n    };\n\n    setup() {\n        this.state = useState({\n            barcode: false,\n        });\n        this.barcodeManual = useRef('manualBarcode');\n        // Autofocus processing was blocked because a document already has a focused element.\n        onMounted(() => {\n            this.barcodeManual.el.focus();\n        });\n    }\n\n    /**\n     * Called when press Enter after filling barcode input manually.\n     *\n     * @private\n     * @param {KeyboardEvent} ev\n     */\n    _onKeydown(ev) {\n        if (ev.key === \"Enter\" && this.state.barcode) {\n            this.props.onSubmit(this.state.barcode);\n        }\n    }\n}\n\nexport class ManualBarcodeScanner extends BarcodeDialog {\n    static template = \"stock_barcode.ManualBarcodeScanner\";\n    static components = {\n        ...BarcodeDialog.components,\n        BarcodeInput,\n    };\n}", "/** @odoo-module **/\n\nimport LineComponent from './line';\n\nexport default class PackageLineComponent extends LineComponent {\n    static props = [\"displayUOM\", \"line\", \"openPackage\"];\n    static template = \"stock_barcode.PackageLineComponent\";\n\n    get isComplete() {\n        return this.qtyDone == this.qtyDemand;\n    }\n\n    get isSelected() {\n        return this.line.package_id.id === this.env.model.lastScanned.packageId;\n    }\n\n    get qtyDemand() {\n        return this.props.line.reservedPackage ? 1 : false;\n    }\n\n    get qtyDone() {\n        const reservedQuantity = this.line.lines.reduce((r, l) => r + l.reserved_uom_qty, 0);\n        const doneQuantity = this.line.lines.reduce((r, l) => r + l.qty_done, 0);\n        if (reservedQuantity > 0) {\n            return doneQuantity / reservedQuantity;\n        }\n        return doneQuantity >= 0 ? 1 : 0;\n    }\n\n    select(ev) {\n        ev.stopPropagation();\n        this.env.model.selectPackageLine(this.line);\n        this.env.model.trigger('update');\n    }\n}\n", "/** @odoo-module **/\n\nimport { Component } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\n\nexport class ProductImageDialog extends Component {\n    static components = { Dialog };\n    static props = {\n        record: Object,\n        close: Function,\n    };\n    static template = \"stock_barcode.ProductImageDialog\";\n\n    setup() {\n        this.source = `/web/image/product.product/${this.props.record.id}/image_1024`;\n        this.title = this.props.record.display_name;\n    }\n}\n", "/** @odoo-module */\n\nimport { KanbanController } from '@web/views/kanban/kanban_controller';\nimport { useBus, useService } from '@web/core/utils/hooks';\nimport { onMounted } from \"@odoo/owl\";\n\nexport class StockBarcodeKanbanController extends KanbanController {\n    setup() {\n        super.setup(...arguments);\n        this.barcodeService = useService('barcode');\n        useBus(this.barcodeService.bus, 'barcode_scanned', (ev) => this._onBarcodeScannedHandler(ev.detail.barcode));\n        onMounted(() => {\n            document.activeElement.blur();\n        });\n    }\n\n    openRecord(record) {\n        this.actionService.doAction('stock_barcode.stock_barcode_picking_client_action', {\n            additionalContext: { active_id: record.resId },\n        });\n    }\n\n    async createRecord() {\n        const action = await this.model.orm.call(\n            'stock.picking',\n            'action_open_new_picking',\n            [], { context: this.props.context }\n        );\n        if (action) {\n            return this.actionService.doAction(action);\n        }\n        return super.createRecord(...arguments);\n    }\n\n    // --------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Called when the user scans a barcode.\n     *\n     * @param {String} barcode\n     */\n    async _onBarcodeScannedHandler(barcode) {\n        const kwargs = { barcode, context: this.props.context };\n        const res = await this.model.orm.call(this.props.resModel, 'filter_on_barcode', [], kwargs);\n        if (res.action) {\n            this.actionService.doAction(res.action);\n        } else if (res.warning) {\n            const params = { title: res.warning.title, type: 'danger' };\n            this.model.notification.add(res.warning.message, params);\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { KanbanRenderer } from '@web/views/kanban/kanban_renderer';\nimport { ManualBarcodeScanner } from \"../components/manual_barcode\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\nimport { useService } from '@web/core/utils/hooks';\nimport { markup, onWillStart } from \"@odoo/owl\";\n\nexport class StockBarcodeKanbanRenderer extends KanbanRenderer {\n    static template = \"stock_barcode.KanbanRenderer\";\n    setup() {\n        super.setup(...arguments);\n        this.barcodeService = useService('barcode');\n        this.dialogService = useService(\"dialog\");\n        this.resModel = this.props.list.model.config.resModel;\n        this.displayTransferProtip = this.resModel === 'stock.picking';\n        onWillStart(this.onWillStart);\n    }\n\n    openManualBarcodeDialog() {\n        this.dialogService.add(ManualBarcodeScanner, {\n            facingMode: \"environment\",\n            onResult: (barcode) => {\n                this.barcodeService.bus.trigger(\"barcode_scanned\", { barcode });\n            },\n            onError: () => {},\n        });\n    }\n\n    async onWillStart() {\n        this.packageEnabled = await user.hasGroup('stock.group_tracking_lot');\n        this.trackingEnabled = await user.hasGroup('stock.group_production_lot');\n    }\n\n    get transferTip() {\n        if (this.trackingEnabled) {\n            if (this.packageEnabled) {\n                return _t(\n                    \"Scan a %(bold_start)s transfer%(bold_end)s, a %(bold_start)s product%(bold_end)s, a %(bold_start)s lot %(bold_end)s or a %(bold_start)s package %(bold_end)s to filter your records\",\n                    { bold_start: markup(\"<b>\"), bold_end: markup(\"</b>\") }\n                );\n            }\n            return _t(\n                \"Scan a %(bold_start)s transfer%(bold_end)s, a %(bold_start)s product%(bold_end)s, or a %(bold_start)s lot %(bold_end)s to filter your records\",\n                { bold_start: markup(\"<b>\"), bold_end: markup(\"</b>\") }\n            );\n        }\n        if (this.packageEnabled) {\n            return _t(\n                \"Scan a %(bold_start)s transfer%(bold_end)s, a %(bold_start)s product%(bold_end)s, or a %(bold_start)s package %(bold_end)s to filter your records\",\n                { bold_start: markup(\"<b>\"), bold_end: markup(\"</b>\") }\n            );\n        }\n        return _t(\n            \"Scan a %(bold_start)s transfer %(bold_end)s or a %(bold_start)s product %(bold_end)s to filter your records\",\n            { bold_start: markup(\"<b>\"), bold_end: markup(\"</b>\") }\n        );\n    }\n}\n", "/** @odoo-module */\n\nimport { kanbanView } from '@web/views/kanban/kanban_view';\nimport { registry } from \"@web/core/registry\";\nimport { StockBarcodeKanbanController } from './stock_barcode_kanban_controller';\nimport { StockBarcodeKanbanRenderer } from './stock_barcode_kanban_renderer';\n\nexport const stockBarcodeKanbanView = Object.assign({}, kanbanView, {\n    Controller: StockBarcodeKanbanController,\n    Renderer: StockBarcodeKanbanRenderer,\n});\nregistry.category(\"views\").add(\"stock_barcode_list_kanban\", stockBarcodeKanbanView);\n", "/** @odoo-module **/\n\nimport { rpc } from \"@web/core/network/rpc\";\n\nexport default class LazyBarcodeCache {\n    constructor(cacheData) {\n        this.dbIdCache = {}; // Cache by model + id\n        this.dbBarcodeCache = {}; // Cache by model + barcode\n        this.dbQuantCache = {}; // Cache by model + quant_id\n        this.missingBarcodesCache = new Set();\n        this.missingBarcodeKeyCache = new Set(); // Used as a cache by `_getMissingRecord`\n        this.barcodeFieldByModel = {\n            'stock.location': 'barcode',\n            'product.product': 'barcode',\n            'product.packaging': 'barcode',\n            'stock.package.type': 'barcode',\n            'stock.picking': 'name',\n            'stock.quant.package': 'name',\n            'stock.lot': 'name', // Also ref, should take in account multiple fields ?\n        };\n        this.gs1LengthsByModel = {\n            'product.product': 14,\n            'product.packaging': 14,\n            'stock.location': 13,\n            'stock.quant.package': 18,\n        };\n        // If there is only one active barcode nomenclature, set the cache to be compliant with it.\n        if (cacheData['barcode.nomenclature'].length === 1) {\n            this.nomenclature = cacheData['barcode.nomenclature'][0];\n        }\n        this.setCache(cacheData);\n        this.waitingFetch = [];\n    }\n\n    /**\n     * Adds records to the barcode application's cache.\n     *\n     * @param {Object} cacheData each key is a model's name and contains an array of records.\n     */\n    setCache(cacheData) {\n        for (const model in cacheData) {\n            const records = cacheData[model];\n            // Adds the model's key in the cache's DB.\n            if (!this.dbIdCache.hasOwnProperty(model)) {\n                this.dbIdCache[model] = {};\n            }\n            if (!this.dbBarcodeCache.hasOwnProperty(model)) {\n                this.dbBarcodeCache[model] = {};\n            }\n            // Adds the record in the cache.\n            const barcodeField = this._getBarcodeField(model);\n            for (const record of records) {\n                this.dbIdCache[model][record.id] = record;\n                if (model === \"stock.quant\") {\n                    const { product_id, location_id } = record;\n                    if (!this.dbQuantCache[product_id]) {\n                        this.dbQuantCache[product_id] = {};\n                    }\n                    if (!this.dbQuantCache[product_id][location_id]) {\n                        this.dbQuantCache[product_id][location_id] = [];\n                    }\n                    this.dbQuantCache[product_id][location_id].push(record);\n                } else if (model === \"product.product\" && cacheData.hasOwnProperty(\"stock.quant\")) {\n                    if (!this.dbQuantCache[record.id]) {\n                        this.dbQuantCache[record.id] = {};\n                    }\n                }\n                if (barcodeField) {\n                    const barcode = record[barcodeField];\n                    if (!this.dbBarcodeCache[model][barcode]) {\n                        this.dbBarcodeCache[model][barcode] = [];\n                    }\n                    if (!this.dbBarcodeCache[model][barcode].includes(record.id)) {\n                        this.dbBarcodeCache[model][barcode].push(record.id);\n                        if (this.nomenclature && this.nomenclature.is_gs1_nomenclature && this.gs1LengthsByModel[model]) {\n                            this._setBarcodeInCacheForGS1(barcode, model, record);\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Get record from the cache, throw a error if we don't find in the cache\n     * (the server should have return this information).\n     *\n     * @param {int} id id of the record\n     * @param {string} model model_name of the record\n     * @param {boolean} [copy=true] if true, returns a deep copy (to avoid to write the cache)\n     * @returns copy of the record send by the server (fields limited to _get_fields_stock_barcode)\n     */\n    getRecord(model, id, raiseErrorIfMissing=true) {\n        if (!this.dbIdCache.hasOwnProperty(model)) {\n            if (raiseErrorIfMissing) {\n                throw new Error(`Model ${model} doesn't exist in the cache`);\n            }\n            return null;\n        }\n        if (!this.dbIdCache[model].hasOwnProperty(id)) {\n            if (raiseErrorIfMissing) {\n                throw new Error(`Record ${model} with id=${id} doesn't exist in the cache, it should return by the server`);\n            }\n            return null;\n        }\n        const record = this.dbIdCache[model][id];\n        return JSON.parse(JSON.stringify(record));\n    }\n\n    async getQuants(product, location_id, params={}) {\n        const lot_id = params.lot_id?.id || params.lot_id || false;\n        const package_id = params.package_id?.id || params.package_id || false;\n        const { lot_name, owner_id = false } = params;\n        let quantsByProduct = this.dbQuantCache[product.id];\n\n        if (!quantsByProduct) {\n            const domain = [\n               [\"product_id\", \"=\", product.id],\n               [\"location_id.usage\", \"=\", \"internal\"],\n            ];\n            const result = await rpc(\"/stock_barcode/get_quants\", { domain });\n            if (result) {\n                this.setCache(result.records);\n                quantsByProduct = this.dbQuantCache[product.id];\n                if (!quantsByProduct) {\n                    // No quant found after fetch.\n                    this.dbQuantCache[product.id] = [];\n                    return [];\n                }\n            }\n        }\n        let quants = [];\n        if (location_id) {\n            const quantsByLocation = quantsByProduct[location_id];\n            if (quantsByLocation) {\n                quants.push(...quantsByLocation);\n            } else {\n                this.dbQuantCache[product.id][location_id] = quantsByLocation;\n            }\n        } else {\n            for (const quantsByLocation of Object.values(quantsByProduct)) {\n                quants.push(...quantsByLocation);\n            }\n        }\n        if (!lot_id && !lot_name && !package_id && !owner_id) {\n            // Return all product's quants in the given location.\n            return quants;\n        }\n        // Return only the quant with the right lot, package, and/or owner, or no quant at all.\n        if (lot_id) {\n            quants = quants.filter((quant) => quant.lot_id === lot_id);\n        } else if (lot_name) {\n            const filters = { \"stock.lot\": { product_id: product.id }};\n            const lot = await this.getRecordByBarcode(lot_name, \"stock.lot\", filters);\n            if (!lot) {\n                // If there is no existing lot, there is no existing quant.\n                return [];\n            }\n            quants = quants.filter((quant) => quant.lot_id === lot.id);\n        }\n        if (owner_id) {\n            quants = quants.filter((quant) => quant.owner_id === owner_id);\n        }\n        if (package_id) {\n            quants = quants.filter((quant) => quant.package_id === package_id);\n        }\n        return quants;\n    }\n\n    /**\n     * @param {string} barcode barcode to match with a record\n     * @param {string} [model] model name of the record to match (if empty search on all models)\n     * @param {boolean} [onlyInCache] search only in the cache\n     * @param {Object} [filters]\n     * @returns copy of the record send by the server (fields limited to _get_fields_stock_barcode)\n     */\n    async getRecordByBarcode(barcode, model = false, options = {}) {\n        const onlyInCache = Boolean(options.onlyInCache);\n        const filters = options.filters || {};\n        const fetchLater = Boolean(options.fetchLater);\n        if (model) {\n            if (!this.dbBarcodeCache.hasOwnProperty(model)) {\n                if (fetchLater) {\n                    this.waitingFetch.push({ barcode, model, options });\n                    return null;\n                }\n                if (onlyInCache) {\n                    return null;\n                }\n                throw new Error(`Model ${model} doesn't exist in the cache`);\n            }\n            if (!this.dbBarcodeCache[model].hasOwnProperty(barcode)) {\n                if (fetchLater) {\n                    this.waitingFetch.push({ barcode, model, options });\n                    return null;\n                }\n                if (onlyInCache) {\n                    return null;\n                }\n                await this._getMissingRecord(barcode, model, filters);\n                return await this.getRecordByBarcode(barcode, model, { onlyInCache: true, filters });\n            }\n            const ids = this.dbBarcodeCache[model][barcode];\n            for (const id of ids) {\n                const record = this.getRecord(model, id);\n                let pass = true;\n                if (filters[model]) {\n                    const fields = Object.keys(filters[model]);\n                    for (const field of fields) {\n                        if (record[field] != filters[model][field]) {\n                            pass = false;\n                            break;\n                        }\n                    }\n                }\n                if (pass) {\n                    return record;\n                }\n            }\n        } else {\n            const result = new Map();\n            // Returns object {model: record} of possible record.\n            const models = Object.keys(this.dbBarcodeCache);\n            for (const model of models) {\n                if (this.dbBarcodeCache[model].hasOwnProperty(barcode)) {\n                    const ids = this.dbBarcodeCache[model][barcode];\n                    for (const id of ids) {\n                        const record = this.dbIdCache[model][id];\n                        let pass = true;\n                        if (filters[model]) {\n                            const fields = Object.keys(filters[model]);\n                            for (const field of fields) {\n                                if (record[field] != filters[model][field]) {\n                                    pass = false;\n                                    break;\n                                }\n                            }\n                        }\n                        if (pass) {\n                            result.set(model, JSON.parse(JSON.stringify(record)));\n                            break;\n                        }\n                    }\n                }\n            }\n            if (result.size < 1) {\n                if (onlyInCache) {\n                    return result;\n                }\n                await this._getMissingRecord(barcode, model, filters);\n                return await this.getRecordByBarcode(barcode, model, { onlyInCache: true, filters });\n            }\n            return result;\n        }\n    }\n\n    _getBarcodeField(model) {\n        if (!this.barcodeFieldByModel.hasOwnProperty(model)) {\n            return null;\n        }\n        return this.barcodeFieldByModel[model];\n    }\n\n    async _getMissingRecord(barcode, model, filters = {}) {\n        const keyCache = JSON.stringify([...arguments]);\n        const missCache = this.missingBarcodeKeyCache;\n        const keyCacheWithoutModel = JSON.stringify([barcode, false, {}]);\n        if (filters) {\n            // If we already tried to find the same model's record for the given barcode but\n            // without the filters, there is no need to try again with the filter.\n            const keyCacheWithoutFilters = JSON.stringify([barcode, model, {}]);\n            if (missCache.has(keyCacheWithoutFilters)) {\n                return false;\n            }\n        }\n        // Check if we already try to fetch this missing record.\n        if (missCache.has(keyCache) || missCache.has(keyCacheWithoutModel)) {\n            return false;\n        }\n        const params = {};\n        if (model) {\n            params.barcodes_by_model = { [model]: [barcode] };\n        } else {\n            params.barcode = barcode;\n        }\n        // Creates and passes a domain if some filters are provided.\n        const domainsByModel = {};\n        for (const filter of Object.entries(filters)) {\n            const modelName = filter[0];\n            const filtersByField = filter[1];\n            domainsByModel[modelName] = [];\n            for (const filterByField of Object.entries(filtersByField)) {\n                if (filterByField[1] instanceof Array) {\n                    domainsByModel[modelName].push([filterByField[0], 'in', filterByField[1]]);\n                } else {\n                    domainsByModel[modelName].push([filterByField[0], '=', filterByField[1]]);\n                }\n            }\n        }\n        params.domains_by_model = domainsByModel;\n        const result = await rpc(\"/stock_barcode/get_specific_barcode_data\", params);\n        this.setCache(result);\n        missCache.add(keyCache);\n    }\n\n    async getMissingRecords(params = {}) {\n        if (!this.waitingFetch.length) {\n            return; // Nothing to fetch.\n        }\n        params.barcodes_by_model = {};\n        for (const data of this.waitingFetch) {\n            const { barcode, model } = data;\n            const keyCache = JSON.stringify([barcode, model, {}]);\n            if (this.missingBarcodeKeyCache.has(keyCache)) {\n                continue; // Avoid already fetched records.\n            }\n            this.missingBarcodeKeyCache.add(keyCache);\n            if (!params.barcodes_by_model[model]) {\n                params.barcodes_by_model[model] = [];\n            }\n            params.barcodes_by_model[model].push(barcode);\n        }\n        if (Object.keys(params.barcodes_by_model).length) {\n            const result = await rpc(\"/stock_barcode/get_specific_barcode_data\", params);\n            this.setCache(result);\n            if (params.forceUnrestrictedSearch) {\n                // Create a list of every found records' barcode.\n                const foundBarcodes = [];\n                const missingBarcodes = new Set();\n                for (const model of Object.keys(result)) {\n                    for (const record of result[model]) {\n                        foundBarcodes.push(record[this.barcodeFieldByModel[model]]);\n                    }\n                }\n                // Put every searched barcodes with no matching records into a set.\n                for (const model of Object.keys(params.barcodes_by_model)) {\n                    for (const barcode of params.barcodes_by_model[model]) {\n                        if (!foundBarcodes.includes(barcode) && !this.missingBarcodesCache.has(barcode)) {\n                            missingBarcodes.add(barcode);\n                            this.missingBarcodesCache.add(barcode);\n                        }\n                    }\n                }\n                // If there are barcodes with no match, make a second RPC but this\n                // time, search for those barcodes with no assigned model.\n                if (missingBarcodes.size) {\n                    const barcodes = [...missingBarcodes];\n                    // Keep in cache the fact those barcodes were search so they won't be in the future.\n                    for (const bc of barcodes) {\n                        this.missingBarcodeKeyCache.add(JSON.stringify([bc, false, {}]));\n                    }\n                    const updatedParams = { ...params, barcodes };\n                    delete updatedParams.barcodes_by_model;\n                    const notRestrictedByModelResult = await rpc(\n                        \"/stock_barcode/get_specific_barcode_data\",\n                        updatedParams);\n                    this.setCache(notRestrictedByModelResult);\n                }\n            }\n        }\n        this.waitingFetch = [];\n    }\n\n    /**\n     * Sets in the cache an entry for the given record with its formatted barcode as key.\n     * The barcode will be formatted (if needed) at the length corresponding to its data part in a\n     * GS1 barcode (e.g.: 14 digits for a product's barcode) by padding with 0 the original barcode.\n     * That makes it easier to find when a GS1 barcode is scanned.\n     * If the formatted barcode is similar to an another barcode for the same model, it will show a\n     * warning in the console (as a clue to find where issue could come from, not to alert the user)\n     *\n     * @param {string} barcode\n     * @param {string} model\n     * @param {Object} record\n     */\n    _setBarcodeInCacheForGS1(barcode, model, record) {\n        const length = this.gs1LengthsByModel[model];\n        if (!barcode || barcode.length >= length || isNaN(Number(barcode))) {\n            // Barcode already has the good length, or is too long or isn't\n            // fully numerical (and so, it doesn't make sense to adapt it).\n            return;\n        }\n        const paddedBarcode = barcode.padStart(length, '0');\n        // Avoids to override or mix records if there is already a key for this\n        // barcode (which means there is a conflict somewhere).\n        if (!this.dbBarcodeCache[model][paddedBarcode]) {\n            this.dbBarcodeCache[model][paddedBarcode] = [record.id];\n        } else if (!this.dbBarcodeCache[model][paddedBarcode].includes(record.id)) {\n            const previousRecordId = this.dbBarcodeCache[model][paddedBarcode][0];\n            const previousRecord = this.getRecord(model, previousRecordId);\n            console.log(\n                `Conflict for barcode %c${paddedBarcode}%c:`, 'font-weight: bold', '',\n                `it could refer for both ${record.display_name} and ${previousRecord.display_name}.`,\n                `\\nThe last one will be used but consider to edit those products barcode to avoid error due to ambiguities.`\n            );\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { registry } from \"@web/core/registry\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { Component, onWillStart, useState } from \"@odoo/owl\";\nimport { ManualBarcodeScanner } from \"../components/manual_barcode\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\nimport { url } from '@web/core/utils/urls';\n\nexport class MainMenu extends Component {\n    static props = { ...standardActionServiceProps };\n    static components = {}\n    static template = \"stock_barcode.MainMenu\";\n\n    setup() {\n        const displayDemoMessage = this.props.action.params.message_demo_barcodes;\n        this.actionService = useService('action');\n        this.dialogService = useService(\"dialog\");\n        this.pwaService = useService(\"pwa\");\n        this.home = useService(\"home_menu\");\n        this.notificationService = useService(\"notification\");\n        this.state = useState({ displayDemoMessage });\n        this.barcodeService = useService('barcode');\n        useBus(this.barcodeService.bus, \"barcode_scanned\", (ev) => this._onBarcodeScanned(ev.detail.barcode));\n\n        onWillStart(async () => {\n            const data = await rpc(\"/stock_barcode/get_main_menu_data\");\n            this.locationsEnabled = data.groups.locations;\n            this.packagesEnabled = data.groups.package;\n            this.trackingEnabled = data.groups.tracking;\n            this.quantCount = data.quant_count;\n            this.soundEnable = data.play_sound;\n            if (this.soundEnable) {\n                const fileExtension = new Audio().canPlayType(\"audio/ogg\") ? \"ogg\" : \"mp3\";\n                this.sounds = {\n                    success: new Audio(url(`/stock_barcode/static/src/audio/success.${fileExtension}`)),\n                };\n                this.sounds.success.load();\n            }\n        });\n    }\n\n    logout() {\n        window.open(`/web/session/logout${ this.pwaService.isScopedApp ? \"?redirect=scoped_app/barcode\" : \"\" }`, \"_self\");\n    }\n\n    openManualBarcodeDialog() {\n        let res;\n        let rej;\n        const promise = new Promise((resolve, reject) => {\n            res = resolve;\n            rej = reject;\n        });\n        this.dialogService.add(ManualBarcodeScanner, {\n            facingMode: \"environment\",\n            onResult: (barcode) => {\n                this._onBarcodeScanned(barcode);\n                res(barcode);\n            },\n            onError: (error) => rej(error),\n        });\n        promise.catch(error => console.log(error))\n        return promise;\n    }\n\n    removeDemoMessage() {\n        this.state.displayDemoMessage = false;\n        const params = {\n            title: _t(\"Don't show this message again\"),\n            body: _t(\"Do you want to permanently remove this message ? \" +\n                    \"It won't appear anymore, so make sure you don't need the barcodes sheet or you have a copy.\"),\n            confirm: () => {\n                rpc('/stock_barcode/rid_of_message_demo_barcodes');\n                location.reload();\n            },\n            cancel: () => {},\n            confirmLabel: _t(\"Remove it\"),\n            cancelLabel: _t(\"Leave it\"),\n        };\n        this.dialogService.add(ConfirmationDialog, params);\n    }\n\n    playSound(soundName) {\n        if (this.soundEnable) {\n            this.sounds[soundName].currentTime = 0;\n            this.sounds[soundName].play();\n        }\n    }\n\n    async _onBarcodeScanned(barcode) {\n        const res = await rpc('/stock_barcode/scan_from_main_menu', { barcode });\n        if (res.action) {\n            this.playSound(\"success\");\n            return this.actionService.doAction(res.action);\n        }\n        this.notificationService.add(res.warning, { type: 'danger' });\n    }\n}\n\nregistry.category('actions').add('stock_barcode_main_menu', MainMenu);\n", "/** @odoo-module **/\n\nimport { BarcodeParser } from \"@barcodes/js/barcode_parser\";\nimport { Mutex } from \"@web/core/utils/concurrency\";\nimport { formatFloat } from \"@web/core/utils/numbers\";\nimport LazyBarcodeCache from '@stock_barcode/lazy_barcode_cache';\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { FNC1_CHAR } from \"@barcodes_gs1_nomenclature/js/barcode_parser\";\nimport { EventBus } from \"@odoo/owl\";\nimport { BarcodeObject } from \"../barcode_object\";\n\nexport default class BarcodeModel extends EventBus {\n    constructor(resModel, resId, services) {\n        super();\n        this.dialogService = useService('dialog');\n        this.orm = services.orm;\n        this.notificationService = services.notification;\n        this.action = services.action;\n        this.resId = resId;\n        this.resModel = resModel;\n        this.unfoldLineKey = false;\n        this.currentSortIndex = 0;\n        this.validateContext = {};\n        // Keeps the history of all barcodes scanned (start with the most recent.)\n        this.scanHistory = [];\n        // Keeps track of list scanned record(s) by type.\n        this.lastScanned = { packageId: false, product: false, sourceLocation: false };\n        this._currentLocation = false; // Reminds the current source when the scanned one is forgotten.\n        this.needSourceConfirmation = false;\n        this.useTrackingNumber = true;\n        this.uriCache = new Set(); // Avoid to scan multiple times the same URI.\n        this.notificationCache = new Set(); // Avoid to display same notification\n    }\n\n    setData(data) {\n        this.actionId = data.actionId;\n        this.cache = new LazyBarcodeCache(data.data.records);\n        const nomenclature = this.cache.getRecord('barcode.nomenclature', data.data.nomenclature_id);\n        nomenclature.rules = [];\n        for (const ruleId of nomenclature.rule_ids) {\n            nomenclature.rules.push(this.cache.getRecord('barcode.rule', ruleId));\n        }\n        this.parser = new BarcodeParser({ nomenclature });\n        BarcodeObject.setEnv(this.cache, this.parser);\n        this.scannedLinesVirtualId = [];\n\n        this.actionMutex = new Mutex();\n        this.config = data.data.config || {};\n        this.groups = data.groups;\n        this.groupingLinesEnabled = this.groups.group_production_lot;\n\n        this.packageTypes = [];\n        if (this.groups.group_tracking_lot) { // Get the package types by barcode.\n            const packageTypes = this.cache.dbBarcodeCache['stock.package.type'] || {};\n            for (const [barcode, ids] of Object.entries(packageTypes)) {\n                this.packageTypes.push([barcode, ids[0]]);\n            }\n        }\n\n        this._createState();\n        this.linesToSave = [];\n        this.selectedLineVirtualId = false;\n\n        // UI stuff.\n        this.name = this._getName();\n        // Barcode's commands are returned by a method for override purpose.\n        this.commands = this._getCommands();\n    }\n\n    // GETTER\n\n    getQtyDone(line) {\n        throw new Error('Not Implemented');\n    }\n\n    getQtyDemand(line) {\n        throw new Error('Not Implemented');\n    }\n\n    getDisplayIncrementBtn(line) {\n        return true;\n    }\n\n    getDisplayIncrementBtnForSerial(line) {\n        return !(line.lot_id || line.lot_name) || this.getQtyDone(line) === 0;\n    }\n\n    getDisplayIncrementPackagingBtn(line) {\n        return false;\n    }\n\n    getActionRefresh(newId) {\n        return {\n            route: '/stock_barcode/get_barcode_data',\n            params: {model: this.resModel, res_id: this.resId || false},\n        };\n    }\n\n    getIncrementQuantity(line) {\n        let remainingQty = this.getLineRemainingQuantity(line);\n        const params = { digits: [false, this.precision], thousandsSep: \"\", decimalPoint: \".\" };\n        return parseFloat(formatFloat(remainingQty, params));\n    }\n\n    getLineRemainingQuantity(line) {\n        return this.getQtyDemand(line) ? this.getQtyDemand(line) - this.getQtyDone(line) : 0;\n    }\n\n    getlotName(line) {\n        return (line.lot_id && line.lot_id.name) || line.lot_name || false;\n    }\n\n    getEditedLineParams(line) {\n        return { currentId: line.id };\n    }\n\n    async apply() {\n        throw new Error('Not Implemented');\n    }\n\n    get barcodeInfo() {\n        throw new Error('Not Implemented');\n    }\n\n    get canCreateNewLot() {\n        return true;\n    }\n\n    get canBeProcessed() {\n        return true;\n    }\n\n    get isValidForBarcodeLookup() {\n        return true;\n    }\n\n    /**\n     * The operation can be validated if there is at least one line.\n     * @returns {boolean}\n     */\n    get canBeValidate() {\n        return this.pageLines.length + this.packageLines.length;\n    }\n\n    get cancelLabel() {\n        return _t(\"Cancel\");\n    }\n\n    get canSelectLocation() {\n        return true;\n    }\n\n    get displayAddProductButton() {\n        return true;\n    }\n\n    get displayApplyButton() {\n        return false;\n    }\n\n    get displayCancelButton() {\n        return false;\n    }\n\n    get displaySignatureButton() {\n        return false;\n    }\n\n    get displayDestinationLocation() {\n        return false;\n    }\n\n    get displayReturnButton() {\n        return false;\n    }\n\n    get useScanDestinationLocation() {\n        return this.displayDestinationLocation;\n    }\n\n    get showReservedSns() {\n        return true;\n    }\n\n    get displayResultPackage() {\n        return false;\n    }\n\n    get displaySourceLocation() {\n        return this.groups.group_stock_multi_locations;\n    }\n\n    get useScanSourceLocation() {\n        return this.displaySourceLocation;\n    }\n\n    displayLineQtyDemand(line) {\n        return this.getQtyDemand(line);\n    }\n\n    groupKey(line) {\n        return `${line.product_id.id}_${line.location_id.id}`;\n    }\n\n    lineCannotBeGrouped(line) {\n        // Don't try to group a line who is not tracked or is already grouped.\n        return line.product_id.tracking === 'none' || line.lines;\n    }\n\n    /**\n     * Returns the page's lines but with tracked products grouped by product id.\n     *\n     * @returns\n     */\n    get groupedLines() {\n        this.groupLines();\n        return this._groupedLines;\n    }\n\n    groupLines() {\n        if (!this.groupingLinesEnabled) {\n            this._groupedLines = this._sortLine(this.pageLines);\n            return this._groupedLines;\n        }\n        const lines = [...this.pageLines];\n        const groupedLinesByKey = {};\n        for (let index = lines.length - 1; index >= 0; index--) {\n            const line = lines[index];\n            if (line.parentLine) {\n                // Remove previous parent line's link.\n                delete line.parentLine;\n            }\n            if (this.lineCannotBeGrouped(line)) {\n                continue;\n            }\n            const key = this.groupKey(line);\n            if (!groupedLinesByKey[key]) {\n                groupedLinesByKey[key] = [];\n            }\n            groupedLinesByKey[key].push(...lines.splice(index, 1));\n        }\n        for (const sublines of Object.values(groupedLinesByKey)) {\n            if (sublines.length === 1) {\n                lines.push(...sublines);\n                continue;\n            }\n            const ids = [];\n            const virtual_ids = [];\n            let [qtyDemand, qtyDone] = [0, 0];\n            for (const subline of sublines) {\n                ids.push(subline.id);\n                virtual_ids.push(subline.virtual_id);\n                qtyDemand += this.getQtyDemand(subline);\n                qtyDone += this.getQtyDone(subline);\n            }\n            const groupedLine = this._groupSublines(sublines, ids, virtual_ids, qtyDemand, qtyDone);\n            lines.push(groupedLine);\n        }\n        // Before to return the line, we sort them to have new lines always on\n        // top and complete lines always on the bottom.\n        this._groupedLines = this._sortLine(lines);\n        return this._groupedLines;\n    }\n\n    get groupedLinesByLocation() {\n        const lines = [].concat(this.groupedLines, this.packageLines);\n        const linesByLocations = []\n        const linesByLocation = {};\n        for (const line of lines) {\n            const lineLoc = line.location_id;\n            if (!linesByLocation[lineLoc.id]) {\n                linesByLocation[lineLoc.id] = {\n                    location: lineLoc,\n                    lines: [],\n                };\n            }\n            if (!linesByLocations.includes(linesByLocation[lineLoc.id])) {\n                linesByLocations.push(linesByLocation[lineLoc.id]);\n            }\n            linesByLocation[lineLoc.id].lines.push(line);\n        }\n        // Sorts groups to ensure that locations will always follow the alphabetical order.\n        linesByLocations.sort((lblA, lblB) => {\n            const [locNameA, locNameB] = [lblA.location.display_name, lblB.location.display_name];\n            return locNameA < locNameB ? -1 : locNameA > locNameB ? 1 : 0;\n        });\n        return linesByLocations\n    }\n\n    get highlightValidateButton() {\n        return false;\n    }\n\n    get isDone() {\n        return false;\n    }\n\n    get isCancelled() {\n        return false;\n    }\n\n    displaySetButton(_line) {\n        return false;\n    }\n\n    /**\n     * Say if the line quantity is not set. Only useful for the inventory adjustment.\n     *\n     * @param {Object} line\n     * @returns {boolean}\n     */\n    IsNotSet(line) {\n        return false;\n    }\n\n    get lastScannedLine() {\n        if (this.scannedLinesVirtualId.length) {\n            const virtualId = this.scannedLinesVirtualId[this.scannedLinesVirtualId.length - 1];\n            return this.currentState.lines.find(l => l.virtual_id === virtualId);\n        }\n        return false;\n    }\n\n    lineCanBeDeleted(line) {\n        return !this.getQtyDemand(line);\n    }\n\n    lineIsFaulty(line) {\n        throw new Error('Not Implemented');\n    }\n\n    lineIsTracked(line) {\n        return this.useTrackingNumber && line.product_id.tracking !== 'none';\n    }\n\n    get location() {\n        if (this.lastScanned.sourceLocation) { // Get last scanned location.\n            return this.cache.getRecord('stock.location', this.lastScanned.sourceLocation.id);\n        }\n        // Get last defined source location (if applicable) or the default location.\n        return this._currentLocation || this._defaultLocation();\n    }\n    set location(location) {\n        this._currentLocation = location;\n        this.lastScanned.sourceLocation = location;\n    }\n\n    get pageLines() {\n        return this.currentState.lines;\n    }\n\n    get packageLines() {\n        return [];\n    }\n\n    get previousScannedLines() {\n        const lines = [];\n        const alreadyDone = [];\n        for (const virtualId of this.scannedLinesVirtualId) {\n            if (alreadyDone.includes(virtualId)) {\n                continue;\n            }\n            alreadyDone.push(virtualId);\n            const foundLine = this.currentState.lines.find(l => l.virtual_id === virtualId);\n            if (foundLine) {\n                lines.push(foundLine);\n            }\n        }\n        if (this.groups.group_stock_packaging) {\n            lines.push(...this.previousScannedLinesByPackage);\n        }\n        return lines;\n    }\n\n    get previousScannedLinesByPackage() {\n        if (this.lastScanned.packageId) {\n            return this.currentState.lines.filter(l => l.package_id && l.package_id.id === this.lastScanned.packageId);\n        }\n        return [];\n    }\n\n    get printButtons() {\n        throw new Error('Not Implemented');\n    }\n\n    get recordIds() {\n        return [this.resId];\n    }\n\n    get selectedLine() {\n        return this.selectedLineVirtualId && this.currentState.lines.find(\n            l => (l.dummy_id || l.virtual_id) === this.selectedLineVirtualId\n        );\n    }\n\n    get useExistingLots() {\n        return true;\n    }\n\n    get validateButtonLabel() {\n        return _t(\"Validate\");\n    }\n\n    // ACTIONS\n\n    /**\n     * @param {integer} [lineId] if provided it checks if the line still exist (selects it or removes it from the lines' list)\n     */\n    async displayBarcodeLines(lineId) {\n        if (lineId) { // If we pass a record id checks if the record still exist.\n            const res = await this.orm.search(this.lineModel, [['id', '=', lineId]]);\n            if (!res.length) { // The record was deleted, we remove the corresponding line.\n                const lineIndex = this.currentState.lines.findIndex(l => l.id == lineId);\n                this.currentState.lines.splice(lineIndex, 1);\n            } else { // If it still exist, selects the record's line.\n                const line = this.currentState.lines.find(line => line.id === lineId);\n                this.selectLine(line);\n            }\n        }\n    }\n\n    /**\n     * Searches for a line in the current source location. Will favor a line with no quantity\n     * (or less than expected) as we assume this kind of line still need to be processed.\n     * @returns {Object | Boolean} Returns a matching line or false.\n     */\n    findLineForCurrentLocation() {\n        if (!this.lastScanned.sourceLocation) {\n            return false; // Can't find anything if no location was scanned.\n        }\n        let foundLine = false;\n        for (const line of this.pageLines) {\n            if (line.location_id.id != this.lastScanned.sourceLocation.id) {\n                continue; // Not the same location.\n            }\n            const [ qtyDone, qtyDemand ] = [this.getQtyDone(line), this.getQtyDemand(line)];\n            if (qtyDone == 0 || (qtyDemand && qtyDone < qtyDemand)) {\n                return line.lot_id ? this._getParentLine(line) : line; // If the line still need to be processed, returns it immediately.\n            }\n            foundLine = !foundLine || qtyDone < this.getQtyDone(foundLine) ? line : foundLine;\n        }\n        return foundLine.lot_id ? this._getParentLine(foundLine) : foundLine;\n    }\n\n    /**\n     * Calls the notification service and plays a sound if the notification's type is \"warning\".\n     * @param {String} message\n     * @param {Object} options\n     */\n    notification(message, options={}) {\n        if (this.notificationCache.has(message)) {\n            return; // Don't display the notification if it's already displayed.\n        }\n        this.notificationCache.add(message);\n        if (options.type === \"danger\") {\n            this.trigger(\"playSound\", \"error\");\n        }\n        return this.notificationService.add(message, options);\n    }\n\n    async refreshCache(records) {\n        this.cache.setCache(records);\n        this._createState();\n    }\n\n    beforeQuit() {\n        return this.save();\n    }\n\n    async save() {\n        const { route, params } = this._getSaveCommand();\n        if (route) {\n            const res = await rpc(route, params);\n            await this.refreshCache(res.records);\n        }\n        this.linesToSave = [];\n    }\n\n    selectLine(line) {\n        if (this.lineCanBeSelected(line) && (!line.virtual_ids || !line.virtual_ids.includes(this.selectedLineVirtualId))) {\n            this._selectLine(line);\n        }\n    }\n\n    selectPackageLine(packageLine) {\n        if (this.lineCanBeSelected(packageLine)) {\n            this.lastScanned.packageId = packageLine.package_id.id;\n        }\n    }\n\n    toggleSublines(line) {\n        const lineKey = this.groupKey(line);\n        this.unfoldLineKey = this.unfoldLineKey === lineKey ? false : lineKey;\n        if (this.unfoldLineKey === lineKey && (!this.selectedLine || this.unfoldLineKey != this.groupKey(this.selectedLine))) {\n            this.selectLine(line);\n        }\n        this.trigger('update');\n    }\n\n    createSingleLinesForPackaging(barcodeData) {\n        return (\n            barcodeData.product.tracking === \"serial\" &&\n            barcodeData.packaging &&\n            (this.useExistingLots || this.canCreateNewLot)\n        );\n    }\n\n    async updateLine(line, args) {\n        let { location_id, lot_id, owner_id, package_id } = args;\n        if (!line) {\n            throw new Error('No line found');\n        }\n        if (!line.product_id && args.product_id) {\n            line.product_id = args.product_id;\n            line.product_uom_id = this.cache.getRecord('uom.uom', args.product_id.uom_id);\n        }\n        if (location_id) {\n            if (typeof location_id === 'number') {\n                location_id = this.cache.getRecord('stock.location', args.location_id);\n            }\n            line.location_id = location_id;\n        }\n        if (lot_id) {\n            if (typeof lot_id === 'number') {\n                lot_id = this.cache.getRecord('stock.lot', args.lot_id);\n            }\n            line.lot_id = lot_id;\n        }\n        if (owner_id) {\n            if (typeof owner_id === 'number') {\n                owner_id = this.cache.getRecord('res.partner', args.owner_id);\n            }\n            line.owner_id = owner_id;\n        }\n        if (package_id) {\n            if (typeof package_id === 'number') {\n                package_id = this.cache.getRecord('stock.quant.package', args.package_id);\n            }\n            line.package_id = package_id;\n        }\n        if (args.lot_name && line.product_id.tracking !== \"none\") {\n            await this.updateLotName(line, args.lot_name);\n        }\n        this._updateLineQty(line, args);\n        this._markLineAsDirty(line);\n    }\n\n    /**\n     * Can be called by the user from the application. As the quantity field hasn't\n     * the same name for all models, this method must be overridden by each model.\n     *\n     * @param {number} virtualId\n     * @param {number} qty Quantity to increment (1 by default)\n     */\n    updateLineQty(virtualId, qty = 1) {\n        throw new Error('Not Implemented');\n    }\n\n    async updateLotName(line, lotName) {\n        // Checks if the tracking number isn't already used.\n        for (const l of this.pageLines) {\n            if (line.virtual_id === l.virtual_id ||\n                line.product_id.tracking !== 'serial' || line.product_id.id !== l.product_id.id) {\n                continue;\n            }\n            if (lotName === l.lot_name || (l.lot_id && lotName === l.lot_id.name)) {\n                this.notification(_t(\"This serial number is already used.\"), { type: \"warning\" });\n                return;\n            }\n        }\n        await this._updateLotName(line, lotName);\n    }\n\n    async validate() {\n        await this.save();\n        const context = this.validateContext;\n        context['barcode_trigger'] = true;\n        const action = await this.orm.call(\n            this.resModel,\n            this.validateMethod,\n            [this.recordIds],\n            { context },\n        );\n        const options = {\n            onClose: ev => this._closeValidate(ev)\n        };\n        if (action && (action.res_model || action.type == \"ir.actions.client\")) {\n            if (action.type == \"ir.actions.client\") {\n                action.params = Object.assign(action.params || {}, options)\n            }\n            this.trigger(\"playSound\");\n            return this.action.doAction(action, options);\n        }\n        return options.onClose();\n    }\n\n    /**\n     * Check if the URI was already scanned.\n     * @param {String} uri\n     * @returns {Boolean}\n     */\n    uriInCache(uri) {\n        return this.uriCache.has(uri);\n    }\n\n    /**\n     * Sometimes, the model receives multiple barcodes as one single string.\n     * This method decomposes it into a list of barcodes.\n     * @param {String} barcode\n     * @returns {Array<String>}\n     */\n    splitBarcode(barcode) {\n        // If the barcode has multiple URI, separate them.\n        const matchedURI = [...barcode.matchAll(/urn:(?:[a-z0-9 -]+:){3} ?[0-9.]+/g)];\n        if (matchedURI.length > 1) {\n            return matchedURI.map(uri => uri[0]);\n        }\n        // If the barcode contains the separator, split it.\n        const sepRegex = RegExp(this.config.barcode_separator_regex);\n        const splitBarcodes = barcode.split(sepRegex).filter(bc => bc);\n        if (splitBarcodes.length > 1) {\n            return [...splitBarcodes];\n        }\n        return [barcode];\n    }\n\n    async processBarcode(barcode, options={}) {\n        if (!barcode) {\n            return; // Do nothing if no barcode given.\n        }\n        const { readingRFID } = options;\n        const barcodes = this.splitBarcode(barcode);\n        if (barcodes.length > 1 && barcode === this._currentBarcode) {\n            // Scanning multiple barcodes at once can take some time and the user may be\n            // tempted to scan again, thinking that the barcodes weren't scanned.\n            // To avoid processing the same group of barcodes multiple times, we keep the\n            // last scanned group of barcodes in memory and nothing will be done if the barcode\n            // is scanned again while previous one is still in process.\n            return;\n        }\n        this._currentBarcode = barcode;\n\n        // Filters out already scanned URI.\n        const filteredBarcodes = [];\n        for (const bc of barcodes) {\n            const matchedURI = bc.match(/^urn:.*$/);\n            if (matchedURI && this.uriInCache(matchedURI[0])) {\n                continue;\n            }\n            filteredBarcodes.push(bc);\n        }\n\n        if (barcodes.length > 1 && !readingRFID) {\n            this.trigger(\"addBarcodesCountToProcess\", filteredBarcodes.length)\n        }\n        // Parse all barcodes.\n        const parsedBarcodes = [];\n        for (const bc of filteredBarcodes) {\n            const barcodeObject = BarcodeObject.forBarcode(bc);\n            await barcodeObject.setRecords();\n            parsedBarcodes.push(barcodeObject);\n        }\n        // Fetch all needed missing data and add them to the cache.\n        await this._getMissingRecords();\n\n        // Link parsed barcodes with missing information to the corresponding record(s).\n        const validBarcodes = [];\n        for (const barcodeObject of parsedBarcodes) {\n            if (barcodeObject.hasMissingRecords) {\n                await barcodeObject.setRecords();\n                if (barcodeObject.isURN && barcodeObject.hasMissingRecords &&\n                    barcodeObject.missingRecords.find(mr => mr.type === \"product\")) {\n                    // This barcode is linked to a product we don't have => We ignore it.\n                    // TODO: what to do with those barcodes ? Missing product => Barcode Lookup ?\n                    // TODO: already scanned SN should be managed here too ?\n                    this.trigger(\"updateBarcodesCountProcessed\");\n                    continue;\n                }\n            }\n            validBarcodes.push(barcodeObject);\n        }\n\n        this.actionMutex.exec(async () => {\n            for (const barcodeObject of validBarcodes) {\n                // TODO: use already parsed barcode in `_processBarcode` instead of parse it again.\n                await this._processBarcode(barcodeObject.rawValue);\n                this.trigger(\"updateBarcodesCountProcessed\");\n            }\n        });\n        this.postProcessBarcode();\n    }\n\n    /**\n     * Called after scanned barcodes were processed to do some cleanings.\n     */\n    postProcessBarcode() {\n        this.trigger(\"clearBarcodesCountProcessed\");\n        this.notificationCache.clear();\n        delete this._currentBarcode;\n    }\n\n    _getMissingRecordsParams() {\n        return {\n            context: { allowed_company_ids: [this._getCompanyId()], },\n            forceUnrestrictedSearch: !this.parser.nomenclature.is_gs1_nomenclature,\n        };\n    }\n\n    async _getMissingRecords() {\n        const params = this._getMissingRecordsParams();\n        await this.cache.getMissingRecords(params);\n    }\n\n    async getGs1Filters(gs1RulesData) {\n        const gs1Filters = {};\n        const productRule = gs1RulesData.find(bc => bc.type === \"product\");\n        if (productRule) {\n            let product = await this.cache.getRecordByBarcode(productRule.value, \"product.product\");\n            if (!product) {\n                const packaging = await this.cache.getRecordByBarcode(productRule.value, \"product.packaging\");\n                if (packaging) {\n                    product = this.cache.getRecord(\"product.product\", packaging.product_id);\n                }\n            }\n            if (product) {\n                gs1Filters[\"stock.lot\"] = { product_id: product.id };\n            }\n        }\n        return gs1Filters;\n    }\n\n    // --------------------------------------------------------------------------\n    // Private\n    // --------------------------------------------------------------------------\n\n    _canOverrideTrackingNumber(line, newLotName) {\n        const lineLotName = line.lot_name || line.lot_id?.name;\n        return !newLotName || !lineLotName || newLotName === lineLotName;\n    }\n\n    _checkBarcode(barcodeData) {\n        return true;\n    }\n\n    async _closeValidate(ev) {\n        if (ev === undefined) {\n            // If all is OK, displays a notification and goes back to the previous page.\n            this.notification(this.validateMessage, { type: \"success\" });\n            this.trigger('history-back');\n        }\n    }\n\n    _convertDataToFieldsParams(args) {\n        throw new Error('Not Implemented');\n    }\n\n    createNewLine(params) {\n        return this._createNewLine(params);\n    }\n\n    /**\n     * Creates a new line with passed parameters, adds it to the barcode app and\n     * to the list of lines to save, then refresh the page.\n     *\n     * @param {Object} params\n     * @param {Object} params.copyOf line to copy fields' value from\n     * @param {Object} params.fieldsParams fields' value to override\n     * @returns {Object} the newly created line\n     */\n    async _createNewLine(params) {\n        if (params.fieldsParams && params.fieldsParams.uom && params.fieldsParams.product_id) {\n            let productUOM = this.cache.getRecord('uom.uom', params.fieldsParams.product_id.uom_id);\n            let paramsUOM = params.fieldsParams.uom;\n            if (paramsUOM.category_id !== productUOM.category_id) {\n                // Not the same UoM's category -> Can't be converted.\n                const message = _t(\n                    \"Scanned quantity uses %(unit)s as its Unit of Measure (UoM), but it is not compatible with the product's UoM (%(productUnit)s).\",\n                    { unit: paramsUOM.name, productUnit: productUOM.name }\n                );\n                this.notification(message, { title: _t(\"Wrong Unit of Measure\"), type: \"danger\" });\n                return false;\n            }\n        }\n        const newLine = Object.assign(\n            {},\n            params.copyOf,\n            this._getNewLineDefaultValues(params.fieldsParams)\n        );\n        const previousIndex = (params.copyOf || this.selectedLine || {}).sortIndex;\n        if (previousIndex !== undefined) {\n            // In case we copy an existing line, we update sort index of following\n            // lines to interpose new line just behind the copied line.\n            const newIndex = previousIndex + 1;\n            for (const line of this.currentState.lines) {\n                if (line.sortIndex >= newIndex) {\n                    line.sortIndex += 1;\n                }\n            }\n            this.currentSortIndex += 1;\n            newLine.sortIndex = newIndex;\n        } else {\n            newLine.sortIndex = this._getLineIndex();\n        }\n        await this.updateLine(newLine, params.fieldsParams);\n        this.currentState.lines.push(newLine);\n        return newLine;\n    }\n\n    async deleteLine(line) {\n        if (!line.id) {\n            // The line doesn't exist in the DB yet => Delete it only in the frontend.\n            const index = this.currentState.lines.findIndex(l => l.virtual_id === line.virtual_id);\n            this.currentState.lines.splice(index, 1);\n            this.linesToSave = this.linesToSave.filter(vId => vId !== line.virtual_id);\n        } else {\n            await this.save();\n            await this.orm.call(this.lineModel, this.deleteLineMethod, [line.id]);\n            this.trigger('refresh');\n        }\n    }\n\n    _shouldCreateLineOnExceed(line) {\n        return true;\n    }\n\n    _defaultLocation() {\n        const lastScannedLocation = this.lastScanned.sourceLocation;\n        return lastScannedLocation || Object.values(this.cache.dbIdCache['stock.location'])[0];\n    }\n\n    _defaultDestLocation() {\n        return undefined;\n    }\n\n    _getCommands() {\n        const commands = {'OCDMENU': this._goToMainMenu.bind(this)};\n        if (!this.isDone) {\n            commands['OBTVALI'] = () => {\n                if (this.canBeValidate) {\n                    this.validate();\n                } else {\n                    this.trigger(\"playSound\", \"error\");\n                }\n            };\n        }\n        return commands;\n    }\n\n    _getLineIndex() {\n        const sortIndex = this.currentSortIndex;\n        this.currentSortIndex++;\n        return sortIndex;\n    }\n\n    _getModelRecord() {\n        return false;\n    }\n\n    _getNewLineDefaultValues(fieldsParams) {\n        return {\n            id: (fieldsParams && fieldsParams.id) || false,\n            virtual_id: this._uniqueVirtualId,\n            location_id: this._defaultLocation(),\n            package_id: false,\n        };\n    }\n\n    _getNewLineDefaultContext() {\n        throw new Error('Not Implemented');\n    }\n\n    _getParentLine(line) {\n        return line && line.parentLine;\n    }\n\n    _getFieldToWrite() {\n        throw new Error('Not Implemented');\n    }\n\n    _fieldToValue(fieldValue) {\n        return typeof fieldValue === 'object' ? fieldValue.id : fieldValue;\n    }\n\n    _getSaveLineCommand() {\n        const commands = [];\n        const fields = this._getFieldToWrite();\n        for (const virtualId of this.linesToSave) {\n            const line = this.currentState.lines.find(l => l.virtual_id === virtualId);\n            if (line.id) { // Update an existing line.\n                const initialLine = this.initialState.lines.find(l => l.virtual_id === line.virtual_id);\n                const changedValues = {};\n                let somethingToSave = false;\n                for (const field of fields) {\n                    const fieldValue = line[field];\n                    const initialValue = initialLine[field];\n                    if (fieldValue !== undefined && (\n                        (['boolean', 'number', 'string'].includes(typeof fieldValue) && fieldValue !== initialValue) ||\n                        (typeof fieldValue === 'object' && fieldValue.id !== initialValue.id)\n                    )) {\n                        changedValues[field] = this._fieldToValue(fieldValue);\n                        somethingToSave = true;\n                    }\n                }\n                if (somethingToSave) {\n                    commands.push([1, line.id, changedValues]);\n                }\n            } else { // Create a new line.\n                commands.push([0, 0, this._createCommandVals(line)]);\n            }\n        }\n        return commands;\n    }\n\n    _getSaveCommand() {\n        throw new Error('Not Implemented');\n    }\n\n    _groupSublines(sublines, ids, virtual_ids, _qtyDemand, _qtyDone) {\n        const sortedSublines = this._sortLine(sublines);\n        // Use the line with lowest ID as the reference (info shown on summary\n        // line and also the move line opened for the form view.)\n        const referenceLine = sortedSublines.reduce((result, line) => {\n            return line.id && (!result.id || (result.id > line.id)) ? line : result;\n        })\n        const groupedLine = Object.assign({}, referenceLine, {\n            ids,\n            lines: sortedSublines,\n            opened: false,\n            virtual_ids,\n        });\n        for (const subline of sublines) {\n            subline.parentLine = groupedLine;\n        }\n        return groupedLine;\n    }\n\n    async _goToMainMenu() {\n        await this.save();\n        this.action.doAction('stock_barcode.stock_barcode_action_main_menu', {\n            clearBreadcrumbs: true,\n        });\n    }\n\n    _createLinesState() {\n        /* Basic lines structure */\n        throw new Error('Not Implemented');\n    }\n\n    /**\n     * Says if a tracked line can be incremented even if there is no tracking number on it.\n     *\n     * @returns {boolean}\n     */\n    _incrementTrackedLine() {\n        return false;\n    }\n\n    _lineIsNotComplete(line) {\n        throw new Error('Not Implemented');\n    }\n\n    /**\n     * Keeps the track of a modified lines to save them later.\n     *\n     * @param {Object} line\n     */\n    _markLineAsDirty(line) {\n        this.scannedLinesVirtualId.push(line.virtual_id);\n        if (!this.linesToSave.includes(line.virtual_id)) {\n            this.linesToSave.push(line.virtual_id);\n        }\n    }\n\n    _moveEntirePackage() {\n        return false;\n    }\n\n    /**\n     * Will parse the given barcode according to the used nomenclature and return\n     * the retrieved data as an object.\n     *\n     * @param {string} barcode\n     * @param {Object} filters For some models, different records can have the same barcode\n     *      (`stock.lot` for example). In this case, these filters can help to get only\n     *      the wanted record by filtering by record's field's value.\n     * @returns {Object} Containing following data:\n     *      - {string} barcode: the scanned barcode\n     *      - {boolean} match: true if the barcode match an existing record\n     *      - {Object} data type: an object for each type of data/record corresponding to the\n     *                 barcode. It could be 'action', 'location', 'product', ...\n     */\n    async _parseBarcode(barcode, filters) {\n        const result = {\n            barcode,\n            match: false,\n        };\n        if (this.commands[barcode]) {\n            result.action = this.commands[barcode];\n            result.match = true;\n            return result;\n        }\n        let parsedBarcode;\n        try {\n            parsedBarcode = this.parser.parse_barcode(barcode);\n        } catch (err) {\n            // The barcode can't be parsed but the error is caught to fallback\n            // on the classic way to handle barcodes.\n            console.log(`%cWarning: error about ${barcode}`, 'text-weight: bold;');\n            console.log(err.message);\n        }\n        if (parsedBarcode) {\n            if (parsedBarcode.length) { // With the GS1 nomenclature, the parsed result is a list.\n                const gs1Filters = await this.getGs1Filters(parsedBarcode);\n                for (const data of parsedBarcode) {\n                    if (data.type === \"lot\" && result.product?.tracking === \"none\") {\n                        continue; // For product not tracked, we don't care about the lot.\n                    }\n                    const parsedData = await this._processGs1Data(data, gs1Filters);\n                    Object.assign(result, parsedData);\n                }\n                if(result.match) {\n                    return result;\n                }\n            } else if (parsedBarcode.type === 'weight') {\n                result.weight = parsedBarcode;\n                result.match = true;\n                barcode = parsedBarcode.base_code;\n            } else if (parsedBarcode.type === 'product' && parsedBarcode.code !== barcode) {\n                // The scanned barcode should match a product but was either an\n                // alias, either converted from UPC-A to EAN-13 (or vice versa.)\n                barcode = parsedBarcode.code;\n                if (this.commands[barcode]) {\n                    result.action = this.commands[barcode];\n                    result.match = true;\n                    return result; // Simple barcode, no more information to retrieve.\n                }\n            }\n        }\n        const fetchedRecord = await this._fetchRecordFromTheCache(barcode, filters, result);\n        return Object.assign(result, fetchedRecord);\n    }\n\n    async _fetchRecordFromTheCache(barcode, filters, data) {\n        const result = data || { barcode, match: false };\n        const recordByData = await this.cache.getRecordByBarcode(barcode, false, { filters });\n        if (recordByData.size > 1) {\n            const message = _t(\n                \"Barcode scan is ambiguous with several model: %s. Use the most likely.\",\n                Array.from(recordByData.keys())\n            );\n            this.notification(message, { type: \"warning\" });\n        }\n\n        if (this.groups.group_stock_multi_locations) {\n            const location = recordByData.get('stock.location');\n            if (location) {\n                this._setLocationFromBarcode(result, location);\n                result.match = true;\n            }\n        }\n\n        if (this.groups.group_tracking_lot) {\n            const packageType = recordByData.get('stock.package.type');\n            const stockPackage = recordByData.get('stock.quant.package');\n            if (stockPackage) {\n                // TODO: should take packages only in current (sub)location.\n                result.package = stockPackage;\n                result.match = true;\n            }\n            if (packageType) {\n                result.packageType = packageType;\n                result.match = true;\n            }\n        }\n\n        const product = recordByData.get('product.product');\n        if (product) {\n            result.product = product;\n            result.match = true;\n        }\n        if (this.groups.group_stock_packaging) {\n            const packaging = recordByData.get('product.packaging');\n            if (packaging) {\n                result.match = true;\n                result.packaging = packaging;\n            }\n        }\n        if (this.useExistingLots) {\n            const lot = recordByData.get('stock.lot');\n            if (lot) {\n                result.lot = lot;\n                result.match = true;\n            }\n        }\n\n        if (!result.match && this.packageTypes.length) {\n            // If no match, check if the barcode begins with a package type's barcode.\n            for (const [packageTypeBarcode, packageTypeId] of this.packageTypes) {\n                if (barcode.indexOf(packageTypeBarcode) === 0) {\n                    result.packageType = await this.cache.getRecord('stock.package.type', packageTypeId);\n                    result.packageName = barcode;\n                    result.match = true;\n                    break;\n                }\n            }\n        }\n        return result;\n    }\n\n    async print(action, method) {\n        await this.save();\n        const options = this._getPrintOptions();\n        if (options.warning) {\n            return this.notification(options.warning, { type: \"warning\" });\n        }\n        if (!action && method) {\n            action = await this.orm.call(\n                this.resModel,\n                method,\n                [[this.resId]]\n            );\n        }\n        this.action.doAction(action, options);\n    }\n\n    async _processGs1Data(data, filters) {\n        const result = {};\n        const { rule, type, value } = data;\n        if ([\"location\", \"location_dest\"].includes(type)) {\n            const location = await this.cache.getRecordByBarcode(value, 'stock.location');\n            if (!location) {\n                return;\n            } else {\n                result.location = location;\n                result.match = true;\n            }\n        } else if (type === \"lot\") {\n            if (this.useExistingLots) {\n                result.lot = await this.cache.getRecordByBarcode(value, 'stock.lot', { filters });\n            }\n            if (!result.lot) { // No existing lot found, set a lot name.\n                result.lotName = value;\n            }\n            if (result.lot || result.lotName) {\n                result.match = true;\n            }\n        } else if (type === \"package\") {\n            const stockPackage = await this.cache.getRecordByBarcode(value, 'stock.quant.package');\n            if (stockPackage) {\n                result.package = stockPackage;\n            } else {\n                // Will be used to force package's name when put in pack.\n                result.packageName = value;\n            }\n            result.match = true;\n        } else if (type === \"package_type\") {\n            const packageType = await this.cache.getRecordByBarcode(value, 'stock.package.type');\n            if (packageType) {\n                result.packageType = packageType;\n                result.match = true;\n            } else {\n                const message = _t(\"An unexisting package type was scanned. This part of the barcode can't be processed.\");\n                this.notification(message, { type: \"warning\" });\n            }\n        } else if (type === \"product\") {\n            const product = await this.cache.getRecordByBarcode(value, 'product.product');\n            if (product) {\n                result.product = product;\n                result.match = true;\n            } else if (this.groups.group_stock_packaging) {\n                const packaging = await this.cache.getRecordByBarcode(value, 'product.packaging');\n                if (packaging) {\n                    result.packaging = packaging;\n                    result.match = true;\n                }\n            }\n        } else if (type === \"quantity\") {\n            result.quantity = value;\n            // The quantity is usually associated to an UoM, but we\n            // ignore this info if the UoM setting is disabled.\n            if (this.groups.group_uom && rule?.associated_uom_id) {\n                result.uom = await this.cache.getRecord('uom.uom', rule.associated_uom_id);\n            }\n            result.match = result.quantity ? true : false;\n        }\n        return result;\n    }\n\n    /**\n     * Starts by parse the barcode and then process each type of barcode data.\n     *\n     * @param {string} barcode\n     * @returns {Promise}\n     */\n    async _processBarcode(barcode) {\n        let barcodeData = {};\n        let currentLine = false;\n        // Creates a filter if needed, which can help to get the right record\n        // when multiple records have the same model and barcode.\n        const filters = {};\n        if (this.selectedLine && this.selectedLine.product_id.tracking !== 'none') {\n            filters['stock.lot'] = {\n                product_id: this.selectedLine.product_id.id,\n            };\n        }\n        // Constrain DB reads to records which belong to the company defined on the open operation\n        filters['all'] = {\n            company_id: [false].concat(this._getCompanyId() || []),\n        };\n        try {\n            barcodeData = await this._parseBarcode(barcode, filters);\n            if (this._shouldSearchForAnotherLot(barcodeData, filters)) {\n                // Retry to parse the barcode without filters in case it matches an existing\n                // record that can't be found because of the filters\n                const lot = await this.cache.getRecordByBarcode(barcode, 'stock.lot');\n                if (lot) {\n                    Object.assign(barcodeData, { lot, match: true });\n                }\n            }\n        } catch (parseErrorMessage) {\n            barcodeData.error = parseErrorMessage;\n        }\n\n        // Keep in memory every scans.\n        this.scanHistory.unshift(barcodeData);\n\n        if (barcodeData.match) { // Makes flash the screen if the scanned barcode was recognized.\n            this.trigger('flash');\n        }\n\n        // Process each data in order, starting with non-ambiguous data type.\n        if (barcodeData.action) { // As action is always a single data, call it and do nothing else.\n            return await barcodeData.action();\n        }\n\n        if (barcodeData.packaging) {\n            Object.assign(barcodeData, this._retrievePackagingData(barcodeData));\n        }\n\n        // Depending of the configuration, the user can be forced to scan a specific barcode type.\n        const check = this._checkBarcode(barcodeData);\n        if (check.error) {\n            return this.notification(check.message, { title: check.title, type: \"danger\" });\n        }\n\n        if (barcodeData.product) { // Remembers the product if a (packaging) product was scanned.\n            this.lastScanned.product = barcodeData.product;\n        }\n\n        if (barcodeData.lot && !barcodeData.product) {\n            Object.assign(barcodeData, this._retrieveTrackingNumberInfo(barcodeData.lot));\n        }\n\n        await this._processLocation(barcodeData);\n        await this._processPackage(barcodeData);\n        if (barcodeData.stopped) {\n            // TODO: Sometime we want to stop here instead of keeping doing thing,\n            // but it's a little hacky, it could be better to don't have to do that.\n            return;\n        }\n\n        if (barcodeData.weight) { // Convert the weight into quantity.\n            barcodeData.quantity = barcodeData.weight.value;\n        }\n\n        // If no product found, take the one from last scanned line if possible.\n        if (!barcodeData.product) {\n            if (barcodeData.quantity) {\n                currentLine = this.selectedLine || this.lastScannedLine;\n            } else if (this.selectedLine && this.selectedLine.product_id.tracking !== 'none') {\n                currentLine = this.selectedLine;\n            } else if (this.lastScannedLine && this.lastScannedLine.product_id.tracking !== 'none') {\n                currentLine = this.lastScannedLine;\n            }\n            if (currentLine) { // If we can, get the product from the previous line.\n                const previousProduct = currentLine.product_id;\n                // If the current product is tracked and the barcode doesn't fit\n                // anything else, we assume it's a new lot/serial number.\n                if (previousProduct.tracking !== 'none' &&\n                    !barcodeData.match && this.canCreateNewLot) {\n                    this.trigger('flash');\n                    barcodeData.lotName = barcode;\n                    barcodeData.product = previousProduct;\n                }\n                if (barcodeData.lot || barcodeData.lotName ||\n                    barcodeData.quantity) {\n                    barcodeData.product = previousProduct;\n                }\n            }\n        }\n        let { product } = barcodeData;\n        if (!product && barcodeData.match && this.parser.nomenclature.is_gs1_nomenclature) {\n            // Special case where something was found using the GS1 nomenclature but no product is\n            // used (eg.: a product's barcode can be read as a lot is starting with 21).\n            // In such case, tries to find a record with the barcode by by-passing the parser.\n            barcodeData = await this._fetchRecordFromTheCache(barcode, filters);\n            if (barcodeData.packaging) {\n                Object.assign(barcodeData, this._retrievePackagingData(barcodeData));\n            } else if (barcodeData.lot) {\n                Object.assign(barcodeData, this._retrieveTrackingNumberInfo(barcodeData.lot));\n            }\n            if (barcodeData.product) {\n                product = barcodeData.product;\n            } else if (barcodeData.match) {\n                await this._processPackage(barcodeData);\n                if (barcodeData.stopped) {\n                    return;\n                }\n            }\n        }\n        if (!product) { // Product is mandatory, if no product, raises a warning.\n            return this.noProductToast(barcodeData);\n        } else if (barcodeData.lot && barcodeData.lot.product_id !== product.id) {\n            delete barcodeData.lot; // The product was scanned alongside another product's lot.\n        }\n        if (barcodeData.weight) { // the encoded weight is based on the product's UoM\n            barcodeData.uom = this.cache.getRecord('uom.uom', product.uom_id);\n        }\n\n        // Searches and selects a line if needed.\n        if (!currentLine || this._shouldSearchForAnotherLine(currentLine, barcodeData)) {\n            currentLine = this._findLine(barcodeData);\n        }\n\n        // Default quantity set to 1 by default if the product is untracked or\n        // if there is a scanned tracking number.\n        if (product.tracking === 'none' || barcodeData.lot || barcodeData.lotName || this._incrementTrackedLine()) {\n            const hasUnassignedQty = currentLine && currentLine.qty_done && !currentLine.lot_id && !currentLine.lot_name;\n            const isTrackingNumber = barcodeData.lot || barcodeData.lotName;\n            const defaultQuantity = isTrackingNumber && hasUnassignedQty ? 0 : 1;\n            barcodeData.quantity = barcodeData.quantity || defaultQuantity;\n            if (product.tracking === 'serial' && barcodeData.quantity > 1 && (barcodeData.lot || barcodeData.lotName)) {\n                barcodeData.quantity = 1;\n                this.notification(\n                    _t(`A product tracked by serial numbers can't have multiple quantities for the same serial number.`),\n                    { type: 'danger' }\n                );\n            }\n        }\n\n        if ((barcodeData.lotName || barcodeData.lot) && product) {\n            const lotName = barcodeData.lotName || barcodeData.lot.name;\n            for (const line of this.currentState.lines) {\n                if (line.product_id.id !== product.id) {\n                    continue; // The same SN can be scanned for different product.\n                }\n                if (line.product_id.tracking === \"serial\" && this.getQtyDone(line) !== 0 &&\n                    this.getlotName(line) === lotName) {\n                    return this.notification(\n                        _t(\"The scanned serial number %s is already used.\", lotName),\n                        { type: 'danger' }\n                    );\n                }\n            }\n            // Prefills `owner_id` and `package_id` if possible.\n            const prefilledOwner = (!currentLine || (currentLine && !currentLine.owner_id)) && this.groups.group_tracking_owner && !barcodeData.owner;\n            const prefilledPackage = (!currentLine || (currentLine && !currentLine.package_id)) && this.groups.group_tracking_lot && !barcodeData.package;\n            if (this.useExistingLots && (prefilledOwner || prefilledPackage)) {\n                const lotId = (barcodeData.lot && barcodeData.lot.id) || (currentLine && currentLine.lot_id && currentLine.lot_id.id) || false;\n                const locationId = (currentLine && currentLine.location_id && currentLine.location_id.id) || false;\n                const params = {\n                    lot_id: lotId,\n                    lot_name: (!lotId && barcodeData.lotName) || false,\n                };\n                let quants = await this.cache.getQuants(product, locationId, params);\n                if (quants.length && quants.length > 1 && (prefilledPackage || prefilledOwner)) {\n                    // If we have multiple matching quants and we use package and/or consigment,\n                    // give priority to the quants with a package or an owner.\n                    const filteredQuants = quants.filter((quant) => {\n                        return quant.package_id || quant.owner_id;\n                    });\n                    quants = filteredQuants.length ? filteredQuants : quants;\n                }\n                if (quants && quants.length === 1) {\n                    const quant = quants[0];\n                    if (prefilledPackage && quant.package_id) {\n                        barcodeData.package = this.cache.getRecord(\"stock.quant.package\", quant.package_id);\n                    }\n                    if (prefilledOwner && quant.owner_id) {\n                        barcodeData.owner = this.cache.getRecord(\"res.partner\", quant.owner_id);\n                    }\n                }\n            }\n        }\n\n        // Updates or creates a line based on barcode data.\n        if (currentLine) { // If line found, can it be incremented ?\n            let exceedingQuantity = 0;\n            if (product.tracking !== 'serial' && barcodeData.uom && barcodeData.uom.category_id == currentLine.product_uom_id.category_id) {\n                // convert to current line's uom\n                barcodeData.quantity = (barcodeData.quantity / barcodeData.uom.factor) * currentLine.product_uom_id.factor;\n                barcodeData.uom = currentLine.product_uom_id;\n            }\n            // Checks the quantity doesn't exceed the line's remaining quantity.\n            if (currentLine.reserved_uom_qty && product.tracking === 'none') {\n                const remainingQty = currentLine.reserved_uom_qty - currentLine.qty_done;\n                if (barcodeData.quantity > remainingQty && this._shouldCreateLineOnExceed(currentLine)) {\n                    // In this case, lowers the increment quantity and keeps\n                    // the excess quantity to create a new line.\n                    exceedingQuantity = barcodeData.quantity - remainingQty;\n                    barcodeData.quantity = remainingQty;\n                }\n            }\n            if (barcodeData.quantity > 0 || barcodeData.lot || barcodeData.lotName) {\n                const fieldsParams = this._convertDataToFieldsParams(barcodeData);\n                if (barcodeData.uom) {\n                    fieldsParams.uom = barcodeData.uom;\n                }\n                await this.updateLine(currentLine, fieldsParams);\n                this.trigger(\"playSound\", \"success\");\n            }\n            if (exceedingQuantity) { // Creates a new line for the excess quantity.\n                barcodeData.quantity = exceedingQuantity;\n                const fieldsParams = this._convertDataToFieldsParams(barcodeData);\n                if (barcodeData.uom) {\n                    fieldsParams.uom = barcodeData.uom;\n                }\n                currentLine = await this._createNewLine({\n                    copyOf: currentLine,\n                    fieldsParams,\n                });\n            }\n        } else { // No line found, so creates a new one.\n            const fieldsParams = this._convertDataToFieldsParams(barcodeData);\n            if (barcodeData.uom) {\n                fieldsParams.uom = barcodeData.uom;\n            }\n            if (this.createSingleLinesForPackaging(barcodeData)) {\n                for (let lineCount = 0; lineCount < barcodeData.packaging.qty; lineCount++) {\n                    currentLine = await this.createNewLine({fieldsParams});\n                }\n            } else {\n                currentLine = await this.createNewLine({fieldsParams});\n            }\n            if(currentLine){\n                this.trigger(\"playSound\", \"success\");\n            }\n        }\n\n        // And finally, if the scanned barcode modified a line, selects this line.\n        if (currentLine) {\n            this._selectLine(currentLine);\n        }\n\n        const matchedURI = barcode.match(/^urn:.*$/);\n        if (matchedURI) {\n            // If the process goes right and the scanned barcode is an URI, add\n            // it to the cache to avoid scanning it a second time.\n            this.uriCache.add(barcode);\n        }\n        this.trigger('update');\n    }\n\n    noProductToast(barcodeData) {\n        if (!barcodeData.error) {\n            if (this.groups.group_tracking_lot) {\n                barcodeData.error = _t(\"You are expected to scan one or more products or a package available at the picking location\");\n            } else {\n                barcodeData.error = _t(\"This product doesn't exist.\");\n            }\n        }\n        return this.notification(barcodeData.error, { type: \"danger\" });\n    }\n\n    async _processLocation(barcodeData) {\n        if (barcodeData.location) {\n            await this._processLocationSource(barcodeData);\n            this.trigger(\"playSound\", \"success\");\n            this.trigger('update');\n        }\n    }\n\n    async _processLocationSource(barcodeData) {\n        this.location = barcodeData.location;\n        barcodeData.stopped = true;\n        // Unselects the line.\n        this.selectedLineVirtualId = false;\n        this.lastScanned.packageId = false;\n    }\n\n    async _processPackage(barcodeData) {\n        throw new Error('Not Implemented');\n    }\n\n    /**\n     * This method cleans the barcode in case the parser use the GS1 nomenclature, removing the\n     * parentheses and the extra spaces (helping for human readability but not valid).\n     * E.g.: (01) 00001234567895 (10) lot-abc -> 0100001234567895\\x1D10lot-abc\n     *\n     * @param {string} barcode\n     * @returns {string} barcode\n     */\n    cleanBarcode (barcode) {\n        if (this.parser.nomenclature.is_gs1_nomenclature) {\n            barcode = barcode.replace(/[( ]([0-9]+)[)]/g, `${FNC1_CHAR}$1`);\n            if (barcode[0] === FNC1_CHAR) {\n                barcode = barcode.slice(1, barcode.length);\n            }\n        }\n        return barcode;\n    }\n\n    lineCanBeSelected() {\n        return true;\n    }\n\n    lineCanBeEdited() {\n        return true;\n    }\n\n    /**\n     * Check if a given line can be taken depending of the current location (if no current location,\n     * it will always be true).\n     * @param {Object} line\n     * @returns {Boolean}\n     */\n    lineCanBeTakenFromTheCurrentLocation(line) {\n        return this.lineIsInTheCurrentLocation(line);\n    }\n\n    lineIsInTheCurrentLocation(line) {\n        return Boolean(\n            !this.groups.group_stock_multi_locations ||\n            !this.lastScanned.sourceLocation || // No current location so we don't care.\n            this.lastScanned.sourceLocation.id == line.location_id.id // Line at the right location.\n        );\n    }\n\n    _retrievePackagingData(barcodeData) {\n        const product = this.cache.getRecord('product.product', barcodeData.packaging.product_id);\n        const uom = this.cache.getRecord('uom.uom', product.uom_id);\n        let quantity = \"quantity\" in barcodeData ? barcodeData.quantity : 1;\n        if (barcodeData.uom && barcodeData.uom.category_id !== uom.category_id) {\n            // In case the scanned quantity uses an UoM not compatible with the\n            // product UoM, we drop it and uses the packaging quantity instead.\n            quantity = barcodeData.packaging.qty\n        } else {\n            // Otherwise, multiply the scanned quantity (or 1 by default) by the package quantity.\n            quantity *= barcodeData.packaging.qty;\n        }\n        return { product, quantity, uom };\n    }\n\n    _retrieveTrackingNumberInfo(lot) {\n        return { product: this.cache.getRecord('product.product', lot.product_id) };\n    }\n\n    _selectLine(line) {\n        const virtualId = line.virtual_id;\n        if (this.selectedLineVirtualId === virtualId) {\n            return; // Don't select the line if it's already selected.\n        }\n        this.selectedLineVirtualId = virtualId;\n        this.lastScanned.destLocation = false;\n    }\n\n    _setLocationFromBarcode(result, location) {\n        result.location = location;\n        return result;\n    }\n\n    _sortingMethod(l1, l2) {\n        // Sort by source location.\n        const sourceLocation1 = l1.location_id.display_name;\n        const sourceLocation2 = l2.location_id.display_name;\n        if (sourceLocation1 < sourceLocation2) {\n            return -1;\n        } else if (sourceLocation1 > sourceLocation2) {\n            return 1;\n        }\n        // Sort by (source) package.\n        const package1 = l1.package_id.name;\n        const package2 = l2.package_id.name;\n        if (package1 < package2) {\n            return -1;\n        } else if (package1 > package2) {\n            return 1;\n        }\n        // Sort by destination location.\n        if (l1.location_dest_id && l2.location_dest_id) {\n            const destinationLocation1 = l1.location_dest_id.display_name;\n            const destinationLocation2 = l2.location_dest_id.display_name;\n            if (destinationLocation1 < destinationLocation2) {\n                return -1;\n            } else if (destinationLocation1 > destinationLocation2) {\n                return 1;\n            }\n        }\n        // Sort by result package.\n        if (l1.result_package_id && l2.result_package_id) {\n            const resultPackage1 = l1.result_package_id.name;\n            const resultPackage2 = l2.result_package_id.name;\n            if (resultPackage1 < resultPackage2) {\n                return -1;\n            } else if (resultPackage1 > resultPackage2) {\n                return 1;\n            }\n        }\n        // Sort by product's category.\n        const categ1 = l1.product_category_name;\n        const categ2 = l2.product_category_name;\n        if (categ1 < categ2) {\n            return -1;\n        } else if (categ1 > categ2) {\n            return 1;\n        }\n        // Sort by product's display name.\n        const product1 = l1.product_id.display_name;\n        const product2 = l2.product_id.display_name;\n        if (product1 < product2) {\n            return -1;\n        } else if (product1 > product2) {\n            return 1;\n        }\n        return 0;\n    }\n\n    /**\n     * Sorts the lines to have new lines always on top and complete lines always on the bottom.\n     *\n     * @param {Array<Object>} lines\n     * @returns {Array<Object>}\n     */\n    _sortLine(lines) {\n        return lines.sort((l1, l2) => {\n            return l1.sortIndex > l2.sortIndex ? 1 : -1;\n        });\n    }\n\n    _findLine(barcodeData) {\n        let foundLine = false;\n        const {lot, lotName, product} = barcodeData;\n        const quantPackage = barcodeData.package;\n        const dataLotName = lotName || (lot && lot.name) || false;\n        let pageLines = [...this.pageLines]\n        // If a line is selected, unshift it to the first position to start the search by it\n        if (this.selectedLineVirtualId) {\n            const selectedLineIndex = pageLines.findIndex(line => line.virtual_id == this.selectedLineVirtualId);\n            if (selectedLineIndex > -1) {\n                pageLines.splice(selectedLineIndex, 1);\n                pageLines.unshift(this.pageLines[selectedLineIndex]);\n            }\n        }\n        for (const line of pageLines) {\n            const lineLotName = this.getlotName(line);\n            if (line.product_id.id !== product.id) {\n                continue; // Not the same product.\n            }\n            if (quantPackage && (!line.package_id || line.package_id.id !== quantPackage.id)) {\n                continue; // Not the expected package.\n            }\n            if (line.product_id.tracking !== \"none\" && !this._canOverrideTrackingNumber(line, dataLotName)) {\n                continue; // Not the same lot.\n            }\n            if (line.product_id.tracking === 'serial') {\n                if (this.getQtyDone(line) >= 1 && lineLotName) {\n                    continue; // Line tracked by serial numbers with quantity & SN.\n                } else if (dataLotName && this.getQtyDone(line) > 1) {\n                    continue; // Can't add a SN on a line where multiple qty. was previously added.\n                }\n            }\n            if ((\n                    !dataLotName || !lineLotName || dataLotName !== lineLotName\n                ) && (\n                    line.qty_done && line.qty_done >= line.reserved_uom_qty &&\n                    (line.product_id.tracking === \"none\" || lineLotName) &&\n                    line.id && (!this.selectedLine || line.virtual_id != this.selectedLine.virtual_id)\n                )) {\n                    // Has enough quantity (and another lot is set if the line's product is tracked)\n                    // and the line wasn't explicitly selected.\n                    continue;\n            }\n            if (this._lineCannotBeTaken(line)) {\n                continue;\n            }\n            if (this._lineIsNotComplete(line)) {\n                if (this.lineCanBeTakenFromTheCurrentLocation(line)) {\n                    // Found a uncompleted compatible line, stop searching if it has the same location\n                    // than the scanned one (or if no location was scanned).\n                    foundLine = line;\n                    if ((this.lineIsInTheCurrentLocation(line)) &&\n                        (line.product_id.tracking === 'none' || !dataLotName || dataLotName === lineLotName)) {\n                        // In case of tracked product, stop searching only if no\n                        // LN/SN was scanned or if it's the same.\n                        break;\n                    }\n                } else if (this.needSourceConfirmation && foundLine && !this._lineIsNotComplete(foundLine)) {\n                    // Found a empty line in another location, we should take it but depending of\n                    // the config, maybe we can't (location should be confirmed first).\n                    // That said, we already found another line but if it's completed, forget we\n                    // found it to avoid to create a new line in the current location because it's\n                    // basicaly the same than increment the other line found in another location.\n                    foundLine = false;\n                    continue;\n                }\n            }\n            // If all the previous checks were passed, the line can be considered\n            // as the found line. That said, if another line was already found,\n            // it can be tricky to know which one we want to prioritize.\n            if (!foundLine) {\n                // The line matches but there could be a better candidate, so keep searching.\n                // If multiple lines can match, prioritises the one at the right location (if a\n                // location source was previously selected) or the selected one if relevant.\n                const currentLocationId = this.lastScanned.sourceLocation && this.lastScanned.sourceLocation.id;\n                if (this.selectedLine && this.selectedLine.virtual_id === line.virtual_id && (\n                    !currentLocationId || !foundLine || foundLine.location_id.id != currentLocationId)) {\n                    foundLine = this.lineCanBeTakenFromTheCurrentLocation(line) ? line : foundLine;\n                } else if (!foundLine || (currentLocationId &&\n                        foundLine.location_id.id != currentLocationId &&\n                        line.location_id.id == currentLocationId)) {\n                    foundLine = this.lineCanBeTakenFromTheCurrentLocation(line) ? line : foundLine;\n                }\n            } else if (this._lineIsNotComplete(foundLine)) {\n                // If previous line is not completed, no reason to prioritize the current one.\n                continue;\n            } else if (this._lineIsNotComplete(line)) {\n                // If previous line is completed and current one is not, prioritize the current one.\n                foundLine = line;\n            } else if (this.lineIsSelected(line) ||\n                (!this.lineIsSelected(foundLine) && this.lineBelongsToSelectedLine(line))\n            ) {\n                // If both previous found line and current line are completed, prioritize the\n                // current one only if it's the selected line (or on of its sublines.)\n                foundLine = line;\n            }\n        }\n        return foundLine;\n    }\n\n    lineBelongsToSelectedLine(line) {\n        if (!this.selectedLine) {\n            return false;\n        }\n        const selectedGroupedLine = this._getParentLine(this.selectedLine);\n        return selectedGroupedLine && selectedGroupedLine.virtual_ids.includes(line.virtual_id);\n    }\n\n    /**\n     * Intended to be used only by `_findLine`.\n     * Depending of the model, they can have additional conditions to know if a\n     * line can be took when a barcode is scanned. This method is meant to be overriden.\n     * @param {Object} _line\n     * @returns {Boolean}\n     */\n    _lineCannotBeTaken(line) {\n        return !this.lineCanBeTakenFromTheCurrentLocation(line);\n    }\n\n    lineIsSelected(line) {\n        return (line.dummy_id || line.virtual_id) === this.selectedLineVirtualId;\n    }\n\n    _shouldSearchForAnotherLot(barcodeData, filters) {\n        return !barcodeData.match && filters['stock.lot'] &&\n            !this.canCreateNewLot && this.useExistingLots\n    }\n\n    _shouldSearchForAnotherLine(line, barcodeData) {\n        if (line.product_id.id !== barcodeData.product.id) {\n            return true;\n        }\n        if (barcodeData.product.tracking === 'serial' && this.getQtyDone(line) > 0) {\n            return true;\n        }\n        const {lot, lotName} = barcodeData;\n        const dataLotName = lotName || (lot && lot.name) || false;\n        const lineLotName = this.getlotName(line);\n        if (dataLotName && lineLotName && dataLotName !== lineLotName) {\n            return true;\n        }\n        const parentLine = this._getParentLine(line);\n        // If the line is a part of a group, we check if the group is fulfilled.\n        const currentLine = parentLine || line;\n        return this.getQtyDone(currentLine) >= this.getQtyDemand(currentLine);\n    }\n\n    get _uniqueVirtualId() {\n        this._lastVirtualId = this._lastVirtualId || 0;\n        return ++this._lastVirtualId;\n    }\n\n    _updateLineQty(line, qty) {\n        throw new Error('Not Implemented');\n    }\n\n    _updateLotName(line, lotName) {\n        throw new Error('Not Implemented');\n    }\n\n    _getName() {\n        return this.cache.getRecord(this.resModel, this.resId).name;\n    }\n\n    // Response -> UI State\n    _createState() {\n        this.record = this._getModelRecord();\n        const lines = this._createLinesState();\n        // Sorts the lines following some criterea and then assign an index for the sort (so they keep the same place).\n        lines.sort(this._sortingMethod.bind(this));\n        for (const line of lines) {\n            line.sortIndex = this._getLineIndex();\n        }\n        this.initialState = { lines };\n        this.currentState = JSON.parse(JSON.stringify(this.initialState)); // Deep copy\n        this.groupLines();\n    }\n\n    _getPrintOptions() {\n        return {};\n    }\n\n    zeroQtyClass(_line) {\n        return \"text-muted\";\n    }\n\n    _getCompanyId() {\n        throw new Error('Not Implemented');\n    }\n\n    _onExit() {\n        return;\n    }\n}\n", "/** @odoo-module **/\n\nimport BarcodeModel from '@stock_barcode/models/barcode_model';\nimport { BackorderDialog } from '../components/backorder_dialog';\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { Deferred } from \"@web/core/utils/concurrency\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { escape } from '@web/core/utils/strings';\nimport { user } from '@web/core/user';\nimport { markup } from '@odoo/owl';\nimport { SignatureDialog } from '@web/core/signature/signature_dialog';\nimport { useService } from \"@web/core/utils/hooks\";\n\n\nexport default class BarcodePickingModel extends BarcodeModel {\n    constructor(resModel, resId, services) {\n        super(resModel, resId, services);\n        this.lineModel = 'stock.move.line';\n        this.showBackOrderDialog = true;\n        this.validateMessage = _t(\"The transfer has been validated\");\n        this.validateMethod = 'button_validate';\n        this.deleteLineMethod = \"unlink\";\n        this.validateContext = {\n            display_detailed_backorder: true,\n            skip_backorder: true,\n        };\n        this.lastScanned.destLocation = false;\n        this.shouldShortenLocationName = true;\n        this.actionName = \"stock_barcode.stock_barcode_picking_client_action\";\n        this.backorderModel = 'stock.picking';\n        this.needSourceConfirmation = {};\n        this.ui = useService('ui');\n    }\n\n    setData(data) {\n        // Picking type's scan restrictions and other barcode's configuration.\n        this.config = data.data.config || {};\n\n        super.setData(...arguments);\n        this._useReservation = this.initialState.lines.some(line => !line.picked);\n        const { use_create_lots, use_existing_lots } = this.record.picking_type_id || {};\n        this.useTrackingNumber = use_create_lots || use_existing_lots;\n        if (!this.useScanDestinationLocation) {\n            this.config.restrict_scan_dest_location = 'no';\n        }\n        this.lineFormViewId = data.data.line_view_id;\n        this.formViewId = data.data.form_view_id;\n        this.scrapViewId = data.data.scrap_view_id;\n        this.packageKanbanViewId = data.data.package_view_id;\n        this.precision = data.data.precision;\n    }\n\n    askBeforeNewLinesCreation(product) {\n        return this._useReservation && product &&\n            !this.currentState.lines.some(line => line.product_id.id === product.id);\n    }\n\n    createNewLine(params) {\n        const product = params.fieldsParams.product_id;\n        if (this.needSourceConfirmation &&\n            this.needSourceConfirmation[this.location.id]?.[product.id]) {\n            const message = _t(\"You are about to take the product %(productName)s from the \" +\n                \"location %(locationName)s but this product isn't reserved in this location.\\n\" +\n                \"Scan the current location to confirm that.\",\n                { productName: product.display_name, locationName: this.location.display_name }\n            );\n            this.needSourceConfirmation[this.location.id][product.id] = false;\n            this.notification(message, { type: \"danger\" });\n            return false;\n        } else if (this.askBeforeNewLinesCreation(product)) {\n            const productName = (product.code ? `[${product.code}] ` : \"\") + product.display_name;\n            if (!this.config.barcode_allow_extra_product) {\n                // No unreserved product can't be added, display a warning.\n                const message = _t(\n                    \"The product %s should not be picked in this operation.\", productName\n                );\n                this.notification(message, { type: \"danger\" });\n                return false;\n            }\n            // Unreserved product can be added but a confirmation is needed.\n            const body = _t(\n                \"Scanned product %s is not reserved for this transfer. Are you sure you want to add it?\",\n                productName\n            );\n            const confirmationPromise = new Promise(resolve => {\n                this.trigger(\"playSound\");\n                this.dialogService.add(ConfirmationDialog, {\n                    title: _t(\"Add extra product?\"),\n                    body,\n                    cancel: () => resolve(false),\n                    confirm: async () => {\n                        const newLine = await this._createNewLine(params);\n                        resolve(newLine);\n                    },\n                    close: () => resolve(false),\n                });\n            });\n            return confirmationPromise;\n        }\n        return super.createNewLine(...arguments);\n    }\n\n    getDisplayIncrementBtn(line) {\n        line = (line.product_id.tracking === \"lot\" && this._getParentLine(line)) || line;\n        return !this.getQtyDemand(line) || this.getQtyDone(line) < this.getQtyDemand(line);\n    }\n\n    getDisplayIncrementBtnForSerial(line) {\n        const lineTrackingNumber = line.lot_id || line.lot_name;\n        return !this.useTrackingNumber || (\n            !this.config.restrict_scan_tracking_number &&\n            lineTrackingNumber && this.getQtyDone(line) === 0);\n    }\n\n    getLineRemainingQuantity(line) {\n        const remainingQty = super.getLineRemainingQuantity(...arguments);\n        const parentLine = (line.product_id.tracking === \"lot\" && this._getParentLine(line)) || line;\n        if (parentLine && this.getQtyDemand(parentLine)) {\n            const parentRemainingQty = this.getQtyDemand(parentLine) - this.getQtyDone(parentLine);\n            if (parentRemainingQty) {\n                return Math.min(Math.max(1, remainingQty), parentRemainingQty);\n            }\n        }\n        return remainingQty;\n    }\n\n    getQtyDone(line) {\n        return line.qty_done;\n    }\n\n    getQtyDemand(line) {\n        return line.reserved_uom_qty || 0;\n    }\n\n    getEditedLineParams(line) {\n        this._setUser();\n        return super.getEditedLineParams(...arguments);\n    }\n\n    getDisplayIncrementPackagingBtn(line) {\n        const packagingQty = line.product_packaging_uom_qty;\n        return packagingQty &&\n            (!this.getQtyDemand(line) || this.getQtyDemand(line) >= this.getQtyDone(line) + packagingQty);\n    }\n\n    displayLineQtyDemand(line) {\n        if (!this.showReservedSns) {\n            return (\n                this.getQtyDemand(line) &&\n                !(this.lineIsTracked(line) && !line.lines && this._getParentLine(line))\n            );\n        }\n        return super.displayLineQtyDemand(line);\n    }\n\n    groupKey(line) {\n        return super.groupKey(...arguments) + `_${line.location_dest_id.id}`;\n    }\n\n    lineCanBeSelected(line) {\n        if (this.selectedLine && this.selectedLine.virtual_id === line.virtual_id) {\n            return true; // We consider an already selected line can always be re-selected.\n        }\n        if (this.config.restrict_scan_source_location && !this.lastScanned.sourceLocation && !line.qty_done) {\n            return false; // Can't select a line if source is mandatory and wasn't scanned yet.\n        }\n        if (line.isPackageLine) {\n            // The next conditions concern product, skips them in case of package line.\n            return super.lineCanBeSelected(...arguments);\n        }\n        const product = line.product_id;\n        if (this.config.restrict_put_in_pack === 'mandatory' && this.selectedLine &&\n            this.selectedLine.qty_done && !this.selectedLine.result_package_id &&\n            this.selectedLine.product_id.id != product.id) {\n            return false; // Can't select another product if a package must be scanned first.\n        }\n        if (this.config.restrict_scan_product && product.barcode) {\n            // If the product scan is mandatory, a line can't be selected if its product isn't\n            // scanned first (as we can't keep track of each line's product scanned state, we\n            // consider a product was scanned if the line has a qty. greater than zero).\n            if (product.tracking === 'none' || !this.config.restrict_scan_tracking_number) {\n                return !this.getQtyDemand(line) || this.getQtyDone(line) || (\n                    this.lastScanned.product && this.lastScanned.product.id === line.product_id.id\n                );\n            } else if (product.tracking != 'none') {\n                return line.lot_name || (line.lot_id && line.qty_done);\n            }\n        }\n        return super.lineCanBeSelected(...arguments);\n    }\n\n    lineCanBeEdited(line) {\n        if (this.config.restrict_scan_product && line.product_id.barcode && !this.getQtyDone(line) && (\n            !this.lastScanned.product || this.lastScanned.product.id != line.product_id.id\n        )) {\n            return false;\n        }\n        if (line.product_id.tracking !== 'none' && this.config.restrict_scan_tracking_number &&\n            !((line.lot_id && line.qty_done) || line.lot_name)) {\n            return false;\n        }\n        return this.lineCanBeSelected(line);\n    }\n\n    lineCanBeTakenFromTheCurrentLocation(line) {\n        // A line with no qty. done can be taken regardless its location (it will be overridden).\n        const res = !this.getQtyDone(line) || super.lineCanBeTakenFromTheCurrentLocation(...arguments);\n        // If source location's scan is mandatory, the source should be confirmed (scanned once\n        // again) to confirm we want to take this product from the current location.\n        if (res && this.config.restrict_scan_source_location &&\n            line.location_id.id !== this.location.id && this.lineIsReserved(line)) {\n            if (this.needSourceConfirmation[this.location.id] === undefined) {\n                this.needSourceConfirmation[this.location.id] = {};\n            }\n            if (!this.scanHistory[1].location || this.scanHistory[1].location.id !== this.location.id) {\n                this.needSourceConfirmation[this.location.id][line.product_id.id] = true;\n                return false;\n            }\n            // The source was scanned just before, no need to confirm it for this product anymore.\n            this.needSourceConfirmation[this.location.id][line.product_id.id] = false;\n        }\n        return res;\n    }\n\n    lineIsReserved(line) {\n        return !line.picked && line.quantity;\n    }\n\n    async updateLine(line, args) {\n        await super.updateLine(...arguments);\n        let { location_id, location_dest_id, result_package_id } = args;\n        if (result_package_id) {\n            if (typeof result_package_id === 'number') {\n                result_package_id = this.cache.getRecord('stock.quant.package', result_package_id);\n                if (result_package_id.package_type_id && typeof result_package_id === 'number') {\n                    result_package_id.package_type_id = this.cache.getRecord('stock.package.type', result_package_id.package_type_id);\n                }\n            }\n            line.result_package_id = result_package_id;\n        }\n        if (!location_id && this.lastScanned.sourceLocation) {\n            line.location_id = this.lastScanned.sourceLocation;\n            if (line.package_id && line.package_id.location_id != line.location_id.id) {\n                line.package_id = false;\n            }\n        }\n        if (location_dest_id) {\n            if (typeof location_dest_id === 'number') {\n                location_dest_id = this.cache.getRecord('stock.location', args.location_dest_id);\n            }\n            line.location_dest_id = location_dest_id;\n        }\n    }\n\n    updateLineQty(virtualId, qty = 1) {\n        this.actionMutex.exec(() => {\n            const line = this.pageLines.find(l => l.virtual_id === virtualId);\n            this.updateLine(line, {qty_done: qty});\n            this.trigger('update');\n        });\n    }\n\n    get backordersDomain() {\n        return [[\"backorder_id\", \"=\", this.resId]];\n    }\n\n    get barcodeInfo() {\n        if (this.isCancelled || this.isDone) {\n            return {\n                class: this.isDone ? 'picking_already_done' : 'picking_already_cancelled',\n                message: this.isDone ?\n                    _t(\"This picking is already done\") :\n                    _t(\"This picking is cancelled\"),\n                icon: \"exclamation-triangle\",\n                warning: true,\n            };\n        }\n        // Takes the parent line if the current line is part of a group.\n        const parentLine = this._getParentLine(this.selectedLine);\n        const line = parentLine && this.getQtyDemand(parentLine) ? parentLine : this.selectedLine;\n        // Defines some messages who can appear in multiple cases.\n        const infos = {\n            scanScrLoc: {\n                message: this.considerPackageLines && !this.config.restrict_scan_source_location ?\n                    _t(\"Scan the source location or a package\") :\n                    _t(\"Scan the source location\"),\n                class: 'scan_src',\n                icon: 'sign-out',\n            },\n            scanDestLoc: {\n                message: _t(\"Scan the destination location\"),\n                class: 'scan_dest',\n                icon: 'sign-in',\n            },\n            scanProductOrDestLoc: {\n                message: this.considerPackageLines ?\n                    _t(\"Scan a product, a package or the destination location.\") :\n                    _t(\"Scan a product or the destination location.\"),\n                class: 'scan_product_or_dest',\n            },\n            scanPackage: {\n                message: this._getScanPackageMessage(line),\n                class: \"scan_package\",\n                icon: 'archive',\n            },\n            scanLot: {\n                message: _t(\"Scan a lot number\"),\n                class: \"scan_lot\",\n                icon: \"barcode\",\n            },\n            scanSerial: {\n                message: _t(\"Scan a serial number\"),\n                class: \"scan_serial\",\n                icon: \"barcode\",\n            },\n            pressValidateBtn: {\n                message: _t(\"Press Validate or scan another product\"),\n                class: 'scan_validate',\n                icon: 'check-square',\n            },\n        };\n        let barcodeInfo = {\n            message: _t(\"Scan a product\"),\n            class: \"scan_product\",\n            icon: \"tags\",\n        };\n        if ((line || this.lastScanned.packageId) && this.groups.group_stock_multi_locations) {\n            if (this.record.picking_type_code === \"outgoing\" && this.useScanSourceLocation) {\n                barcodeInfo = {\n                    message: _t(\"Scan more products, or scan a new source location\"),\n                    class: \"scan_product_or_src\",\n                };\n            } else if (this.config.restrict_scan_dest_location != \"no\") {\n                barcodeInfo = infos.scanProductOrDestLoc;\n            }\n        }\n\n        if (!line && this._moveEntirePackage()) { // About package lines.\n            const packageLine = this.selectedPackageLine;\n            if (packageLine) {\n                if (this._lineIsComplete(packageLine)) {\n                    if (this.config.restrict_scan_source_location && !this.lastScanned.sourceLocation) {\n                        return infos.scanScrLoc;\n                    } else if (this.config.restrict_scan_dest_location != 'no' && !this.lastScanned.destLocation) {\n                        return this.config.restrict_scan_dest_location == 'mandatory' ?\n                            infos.scanDestLoc :\n                            infos.scanProductOrDestLoc;\n                    } else if (this.pageIsDone) {\n                        return infos.pressValidateBtn;\n                    } else {\n                        barcodeInfo.message = _t(\"Scan a product or another package\");\n                        barcodeInfo.class = 'scan_product_or_package';\n                    }\n                } else {\n                    barcodeInfo.message = _t(\"Scan the package %s\", packageLine.result_package_id.name);\n                    barcodeInfo.icon = 'archive';\n                }\n                return barcodeInfo;\n            } else if (this.considerPackageLines && barcodeInfo.class == 'scan_product') {\n                barcodeInfo.message = _t(\"Scan a product or a package\");\n                barcodeInfo.class = 'scan_product_or_package';\n            }\n        }\n        if (barcodeInfo.class === \"scan_product\" && !(line || this.lastScanned.packageId) &&\n            this.config.restrict_scan_source_location && this.lastScanned.sourceLocation) {\n            barcodeInfo.message = _t(\n                \"Scan a product from %s\",\n                this.lastScanned.sourceLocation.name\n            );\n        }\n\n        // About source location.\n        if (this.useScanSourceLocation) {\n            if (!this.lastScanned.sourceLocation && !this.pageIsDone) {\n                return infos.scanScrLoc;\n            } else if (this.lastScanned.sourceLocation && this.lastScanned.destLocation == 'no' &&\n                       line && this._lineIsComplete(line)) {\n                if (this.config.restrict_put_in_pack === 'mandatory' && !line.result_package_id) {\n                    return {\n                        message: _t(\"Scan a package\"),\n                        class: 'scan_package',\n                        icon: 'archive',\n                    };\n                }\n                return infos.scanScrLoc;\n            }\n        }\n\n        if (!line) {\n            if (this.pageIsDone) { // All is done, says to validate the transfer.\n                return infos.pressValidateBtn;\n            } else if (this.config.lines_need_to_be_packed) {\n                const lines = new Array(...this.pageLines, ...this.packageLines);\n                if (lines.every(line => !this._lineIsNotComplete(line)) &&\n                    lines.some(line => this._lineNeedsToBePacked(line))) {\n                        return infos.scanPackage;\n                }\n            }\n            return barcodeInfo;\n        }\n        const product = line.product_id;\n\n        // About tracking numbers.\n        if (product.tracking !== 'none' && (this.record.picking_type_id.use_create_lots || this.record.picking_type_id.use_existing_lots)) {\n            const isLot = product.tracking === \"lot\";\n            if (this.getQtyDemand(line) && (line.lot_id || line.lot_name)) { // Reserved.\n                if (this.getQtyDone(line) === 0) { // Lot/SN not scanned yet.\n                    return isLot ? infos.scanLot : infos.scanSerial;\n                } else if (this.getQtyDone(line) < this.getQtyDemand(line)) { // Lot/SN scanned but not enough.\n                    barcodeInfo = isLot ? infos.scanLot : infos.scanSerial;\n                    barcodeInfo.message = isLot ?\n                        _t(\"Scan more lot numbers\") :\n                        _t(\"Scan another serial number\");\n                    return barcodeInfo;\n                }\n            } else if (!(line.lot_id || line.lot_name)) { // Not reserved.\n                return isLot ? infos.scanLot : infos.scanSerial;\n            }\n        }\n\n        // About package.\n        if (this._lineNeedsToBePacked(line)) {\n            if (this._lineIsComplete(line)) {\n                return infos.scanPackage;\n            }\n            if (product.tracking == 'serial') {\n                barcodeInfo.message = _t(\"Scan a serial number or a package\");\n            } else if (product.tracking == 'lot') {\n                barcodeInfo.message = line.qty_done == 0 ?\n                    _t(\"Scan a lot number\") :\n                    _t(\"Scan more lot numbers or a package\");\n                    barcodeInfo.class = \"scan_lot\";\n            } else {\n                barcodeInfo.message = _t(\"Scan more products or a package\");\n            }\n            return barcodeInfo;\n        }\n\n        if (this.pageIsDone) {\n            barcodeInfo = infos.pressValidateBtn;\n        }\n\n        // About destination location.\n        const lineWaitingPackage = this.groups.group_tracking_lot && this.config.restrict_put_in_pack != \"no\" && !line.result_package_id;\n        if (this.config.restrict_scan_dest_location != 'no' && line.qty_done) {\n            if (this.pageIsDone) {\n                if (this.lastScanned.destLocation) {\n                    return infos.pressValidateBtn;\n                } else {\n                    return this.config.restrict_scan_dest_location == 'mandatory' && this._lineIsComplete(line) ?\n                        infos.scanDestLoc :\n                        infos.scanProductOrDestLoc;\n                }\n            } else if (this._lineIsComplete(line)) {\n                if (lineWaitingPackage) {\n                    barcodeInfo.message = this.config.restrict_scan_dest_location == 'mandatory' ?\n                        _t(\"Scan a package or the destination location\") :\n                        _t(\"Scan a package, the destination location or another product\");\n                } else {\n                    return this.config.restrict_scan_dest_location == 'mandatory' ?\n                        infos.scanDestLoc :\n                        infos.scanProductOrDestLoc;\n                }\n            } else {\n                barcodeInfo = infos.scanProductOrDestLoc;\n                if (product.tracking == 'serial') {\n                    barcodeInfo.message = lineWaitingPackage ?\n                        _t(\"Scan a serial number or a package then the destination location\") :\n                        _t(\"Scan a serial number then the destination location\");\n                } else if (product.tracking == 'lot') {\n                    barcodeInfo.message = lineWaitingPackage ?\n                        _t(\"Scan a lot number or a packages then the destination location\") :\n                        _t(\"Scan a lot number then the destination location\");\n                } else {\n                    barcodeInfo.message = lineWaitingPackage ?\n                        _t(\"Scan a product, a package or the destination location\") :\n                        _t(\"Scan a product then the destination location\");\n                }\n            }\n        }\n\n        return barcodeInfo;\n    }\n\n    get canBeProcessed() {\n        return !['cancel', 'done'].includes(this.record.state);\n    }\n\n    get displaySignatureButton() {\n        return this.record.picking_type_code === 'outgoing' && this.groups.group_stock_sign_delivery;\n    }\n\n    /**\n     * Depending of the config, a transfer can be fully validate even if nothing was scanned (like\n     * with an immediate transfer) or if at least one product was scanned.\n     * @returns {boolean}\n     */\n    get canBeValidate() {\n        if (!this._useReservation) {\n            return super.canBeValidate; // For immediate transfers, doesn't care about any special condition.\n        } else if (!this.config.barcode_validation_full && !this.currentState.lines.some(line => line.qty_done)) {\n            return false; // Can't be validate because \"full validation\" is forbidden and nothing was processed yet.\n        }\n        return super.canBeValidate;\n    }\n\n    get cancelLabel() {\n        return _t(\"Cancel Transfer\");\n    }\n\n    get canCreateNewLot() {\n        return this.record.use_create_lots;\n    }\n\n    get showReservedSns() {\n        if (this.canCreateNewLot && !this.useExistingLots) {\n            return false;\n        }\n        return this.record.picking_type_id.show_reserved_sns;\n    }\n\n    get canPutInPack() {\n        if (this.config.restrict_scan_product) {\n            return this.pageLines.some(line => line.qty_done && !line.result_package_id);\n        }\n        return true;\n    }\n\n    get canScrap() {\n        const { picking_type_code, state } = this.record;\n        return (picking_type_code === \"incoming\" && state === \"done\") ||\n               (picking_type_code === \"outgoing\" && state !== \"done\") ||\n               (picking_type_code === \"internal\");\n    }\n\n    get scrapContext() {\n        const context = this._getNewLineDefaultContext();\n        delete context.force_fullfil_quantity;\n        const moves = this.record.move_ids.map(id => this.cache.getRecord(\"stock.move\", id))\n        context['product_ids'] = moves.map(move => move.product_id);\n        return context;\n    }\n\n    get canSelectLocation() {\n        return !(this.config.restrict_scan_source_location || this.config.restrict_scan_dest_location != 'optional');\n    }\n\n    shouldSplitLine(line) {\n        if (!line.qty_done || !line.reserved_uom_qty || line.qty_done >= line.reserved_uom_qty) {\n            return false; // No need to split a completed line or a line with no reservation.\n        }\n        line = this._getParentLine(line) || line;\n        return line.qty_done && line.reserved_uom_qty && line.qty_done < line.reserved_uom_qty;\n    }\n\n    /**\n     * Splits a line if its qty done is less than reserved.\n     * In case of a grouped line, if there's is a lot id or product tracking is serial,\n     * the new line doesn't need to be splitted since there is an existing line\n     * that will be grouped seperately after location is changed.\n     *\n     * @returns {Boolean|Object} Returns the new splitted line or false if line can't be split.\n    */\n    async splitLine(line) {\n        if (!this.shouldSplitLine(line)) {\n            return false;\n        }\n        // Use line's locations otherwise the picking's locations are used as default locations.\n        const fieldsParams = {\n            location_id: line.location_id.id,\n            location_dest_id: line.location_dest_id.id,\n        };\n        const newLine = await this._createNewLine({ copyOf: line, fieldsParams });\n        delete newLine.parentLine;\n        // Update the reservation of the both old and new lines.\n        newLine.reserved_uom_qty = line.reserved_uom_qty - line.qty_done;\n        line.reserved_uom_qty = line.qty_done;\n        // Be sure the new line has no lot by default.\n        newLine.lot_id = false;\n        newLine.lot_name = false;\n\n        return newLine;\n    }\n\n    /**\n     * The line's destination is changed to the given location, and if the line's reservation isn't\n     * fulfilled, the remaining qties are moved to a new line with the original destination location.\n     *\n     * @param {int} id location's id\n     */\n    async changeDestinationLocation(id, selectedLine) {\n        if (selectedLine.lines && !selectedLine.isPackageLine) {\n            this._clearScanData();\n            return false;\n        }\n        if (!selectedLine.lot_id) {\n            await this.splitLine(selectedLine);\n        }\n        // If the line has no reservation and is grouped with sibling lines,\n        // checks if we can assign to it a part of the reservation.\n        const parentLine = this._getParentLine(selectedLine);\n        if (selectedLine.product_id.tracking === \"lot\" &&\n            parentLine && selectedLine.qty_done && !selectedLine.reserved_uom_qty) {\n            // Searches for a line with uncomplete reservation.\n            const uncompletedLine = parentLine.lines.find(\n                line => line.reserved_uom_qty && line.qty_done < line.reserved_uom_qty\n            );\n            if (uncompletedLine) {\n                // Checks if a portion of the reservation can be assign to the current line.\n                const remainingQty = Math.max(\n                    0, uncompletedLine.reserved_uom_qty - uncompletedLine.qty_done\n                );\n                const stolenReservation = Math.min(remainingQty, selectedLine.qty_done);\n                if (stolenReservation) {\n                    // Assigns the reservation on the current line.\n                    uncompletedLine.reserved_uom_qty -= stolenReservation;\n                    selectedLine.reserved_uom_qty = stolenReservation;\n                }\n            }\n        }\n        selectedLine.location_dest_id = this.cache.getRecord('stock.location', id);\n        this._markLineAsDirty(selectedLine);\n        this._clearScanData();\n        return true;\n    }\n\n    _clearScanData() {\n        this.selectedLineVirtualId = false;\n        this.location = false;\n        this.lastScanned.packageId = false;\n        this.lastScanned.product = false;\n        this.scannedLinesVirtualId = [];\n    }\n\n    get isValidForBarcodeLookup() {\n        if (this?.record.picking_type_code === \"incoming\") {\n            return true;\n        }\n        return false;\n    }\n\n    get considerPackageLines() {\n        return this._moveEntirePackage() && this.packageLines.length;\n    }\n\n    get displayAddProductButton() {\n        return !this._useReservation || this.config.barcode_allow_extra_product;\n    }\n\n    get displayCancelButton() {\n        return !['done', 'cancel'].includes(this.record.state);\n    }\n\n    get displayDestinationLocation() {\n        return this.groups.group_stock_multi_locations &&\n            ['incoming', 'internal'].includes(this.record.picking_type_code)\n    }\n\n    get displayPutInPackButton() {\n        return this.groups.group_tracking_lot && this.config.restrict_put_in_pack != 'no';\n    }\n\n    get displayResultPackage() {\n        return true;\n    }\n\n    get displaySourceLocation() {\n        return super.displaySourceLocation &&\n            ['internal', 'outgoing'].includes(this.record.picking_type_code);\n    }\n\n    get displayReturnButton() {\n        return this.resModel === 'stock.picking' && this.isDone;\n    }\n\n    get useScanSourceLocation() {\n        return super.useScanSourceLocation && this.config.restrict_scan_source_location;\n    }\n\n    get useScanDestinationLocation() {\n        return super.useScanDestinationLocation && this.config.restrict_scan_dest_location != 'no';\n    }\n\n    get displayValidateButton() {\n        return true;\n    }\n\n    get highlightValidateButton() {\n        if (!this.pageLines.length && !this.packageLines.length) {\n            return false;\n        }\n        if (this.config.restrict_scan_dest_location == 'mandatory' &&\n            !this.lastScanned.destLocation && this.selectedLine) {\n            return false;\n        }\n        for (let line of this.pageLines) {\n            line = this._getParentLine(line) || line;\n            if (this._lineIsNotComplete(line)) {\n                return false;\n            }\n        }\n        for (const packageLine of this.packageLines) {\n            if (this._lineIsNotComplete(packageLine)) {\n                return false;\n            }\n        }\n        return Boolean([...this.pageLines, ...this.packageLines].length);\n    }\n\n    get isDone() {\n        return this.record.state === 'done';\n    }\n\n    get isCancelled() {\n        return this.record.state === 'cancel';\n    }\n\n    lineIsFaulty(line) {\n        return (\n            this._useReservation &&\n            line.qty_done > line.reserved_uom_qty &&\n            (this.showReservedSns || !this.lineIsTracked(line))\n        );\n    }\n\n    get moveIds() {\n        return this.record.move_ids;\n    }\n\n    get packageLines() {\n        if (!this._moveEntirePackage()) {\n            return [];\n        }\n        const linesWithPackage = this.currentState.lines.filter(line => line.package_id && line.result_package_id);\n        // Groups lines by package.\n        const groupedLines = {};\n        for (const line of linesWithPackage) {\n            const packageId = line.package_id.id;\n            if (!groupedLines[packageId]) {\n                groupedLines[packageId] = [];\n            }\n            groupedLines[packageId].push(line);\n        }\n        const packageLines = [];\n        for (const key in groupedLines) {\n            // Check if the package is reserved.\n            const reservedPackage = groupedLines[key].every(line => this.lineIsReserved(line));\n            groupedLines[key][0].reservedPackage = reservedPackage;\n            const packageLine = Object.assign({}, groupedLines[key][0], {\n                lines: groupedLines[key],\n                isPackageLine: true,\n            });\n            packageLines.push(packageLine);\n        }\n        return this._sortLine(packageLines);\n    }\n\n    get pageIsDone() {\n        for (const line of this.groupedLines) {\n            if (this._lineIsNotComplete(line) || this._lineNeedsToBePacked(line) ||\n                (line.product_id.tracking != 'none' && !(line.lot_id || line.lot_name))) {\n                return false;\n            }\n        }\n        for (const line of this.packageLines) {\n            if (this._lineIsNotComplete(line)) {\n                return false;\n            }\n        }\n        return Boolean([...this.groupedLines, ...this.packageLines].length);\n    }\n\n    /**\n     * Returns only the lines (filters out the package lines if relevant).\n     * @returns {Array<Object>}\n     */\n     get pageLines() {\n        let lines = super.pageLines;\n        // If we show entire package, we don't return lines with package (they\n        // will be treated as \"package lines\").\n        if (this._moveEntirePackage()) {\n            lines = lines.filter(line => !(line.package_id && line.result_package_id));\n        }\n        return this._sortLine(lines);\n    }\n\n    get previousScannedLinesByPackage() {\n        if (this.lastScanned.packageId) {\n            return this.currentState.lines.filter(l => l.result_package_id.id === this.lastScanned.packageId);\n        }\n        return [];\n    }\n\n    get printButtons() {\n        const buttons = [\n            {\n                name: _t(\"Print Picking Operations\"),\n                class: 'o_print_picking',\n                method: 'do_print_picking',\n            }, {\n                name: _t(\"Print Delivery Slip\"),\n                class: 'o_print_delivery_slip',\n                method: 'action_print_delivery_slip',\n            }, {\n                name: _t(\"Print Barcodes\"),\n                class: 'o_print_barcodes',\n                method: 'action_print_barcode',\n            },\n        ];\n        if (this.groups.group_tracking_lot) {\n            buttons.push({\n                name: _t(\"Print Packages\"),\n                class: 'o_print_packages',\n                method: 'action_print_packges',\n            });\n        }\n\n        return buttons;\n    }\n\n    get reloadingMoveLines() {\n        return this.currentState !== undefined;\n    }\n\n    async save() {\n        if (this.linesToSave.length > 0) {\n            await this._setUser();\n        }\n        return super.save();\n    }\n\n    get selectedPackageLine() {\n        return this.lastScanned.packageId && this.packageLines.find(pl => pl.result_package_id.id == this.lastScanned.packageId);\n    }\n\n    get useExistingLots() {\n        return this.record.use_existing_lots;\n    }\n\n    async uploadSignature({ signatureImage }) {\n        const file = signatureImage.split(\",\")[1];\n\n        this.ui.block();\n        await this.orm.write(this.resModel, [this.resId], {\n            signature: file,\n        });\n        this.ui.unblock();\n        await this.save();\n        this.trigger('refresh');\n    }\n\n    openSignatureDialog(validateAfterSignature = false) {\n        const nameAndSignatureProps = {\n            mode: \"draw\",\n            displaySignatureRatio: 3,\n            signatureType: \"signature\",\n            noInputName: true,\n        };\n        const defaultName = this.record.partner_id?.display_name;\n\n        const dialogProps = {\n            defaultName,\n            nameAndSignatureProps,\n            uploadSignature: async (data) => {\n                await this.uploadSignature(data);\n                if (validateAfterSignature) {\n                    await super.validate();\n                }\n            },\n        };\n        this.dialogService.add(SignatureDialog, dialogProps);\n    }\n\n    get shouldOpenSignatureModal() {\n        const { picking_type_code: pickingTypeCode, signature } = this.record;\n        return pickingTypeCode === 'outgoing' && !signature && this.groups.group_stock_sign_delivery;\n    }\n\n    async validate() {\n        if (this.config.restrict_scan_dest_location == 'mandatory' &&\n            !this.lastScanned.destLocation && this.selectedLine) {\n            return this.notification(_t(\"Destination location must be scanned\"), { type: \"danger\" });\n        }\n        if (this.config.lines_need_to_be_packed &&\n            this.currentState.lines.some(line => this._lineNeedsToBePacked(line))) {\n            return this.notification(_t(\"All products need to be packed\"), { type: \"danger\" });\n        }\n        await this._setUser();\n        if (this.config.create_backorder === 'ask') {\n            // If there are some uncompleted lines, displays the backorder dialog.\n            const uncompletedLines = [];\n            const alreadyChecked = [];\n            let atLeastOneLinePartiallyProcessed = false;\n            for (let line of this.currentState.lines) {\n                line = this._getParentLine(line) || line;\n                if (alreadyChecked.includes(line.virtual_id)) {\n                    continue;\n                }\n                // Keeps track of already checked lines to avoid to check multiple times grouped lines.\n                alreadyChecked.push(line.virtual_id);\n                let qtyDone = line.qty_done;\n                if (qtyDone < line.reserved_uom_qty) {\n                    // Checks if another move line shares the same move id and adds its quantity done in that case.\n                    qtyDone += this.currentState.lines.reduce((additionalQtyDone, otherLine) => {\n                        return otherLine.product_id.id === line.product_id.id\n                            && otherLine.move_id === line.move_id\n                            && !otherLine.reserved_uom_qty ?\n                            additionalQtyDone + otherLine.qty_done : additionalQtyDone\n                    }, 0);\n                    if (qtyDone < line.reserved_uom_qty) { // Quantity done still insufficient.\n                        uncompletedLines.push(line);\n                    }\n                }\n                atLeastOneLinePartiallyProcessed = atLeastOneLinePartiallyProcessed || (qtyDone > 0);\n            }\n            if (this.showBackOrderDialog && atLeastOneLinePartiallyProcessed && uncompletedLines.length) {\n                this.trigger(\"playSound\");\n                return this.dialogService.add(BackorderDialog, {\n                    displayUoM: this.groups.group_uom,\n                    uncompletedLines,\n                    onApply: () => super.validate(),\n                });\n            }\n        }\n        if (this.record.return_id) {\n            this.validateContext = {...this.validateContext, picking_ids_not_to_backorder: this.resId};\n        }\n        if (this.shouldOpenSignatureModal) {\n            this.openSignatureDialog(true);\n            return;\n        }\n        return await super.validate();\n    }\n\n    // -------------------------------------------------------------------------\n    // Private\n    // -------------------------------------------------------------------------\n\n    async _assignEmptyPackage(line, resultPackage) {\n        const fieldsParams = this._convertDataToFieldsParams({ resultPackage });\n        const parentLine = this._getParentLine(line);\n        const targetLines = parentLine ? parentLine.lines : [line]\n        for (const subline of targetLines) { // Assigns the result package on all sibling lines\n            if (subline === line || (subline.qty_done && !subline.result_package_id)) {\n                if (this.shouldSplitLine(subline)) {\n                    // Subline has no package already and is only partially full,\n                    // so we split off the remaining amount into a new move line\n                    const newLine = await this.splitLine(subline);\n                    [newLine.sortIndex, subline.sortIndex] = [subline.sortIndex, newLine.sortIndex]\n                    if (subline === line) {\n                        this.selectLine(newLine);\n                    }\n                }\n                await this.updateLine(subline, fieldsParams);\n            }\n        }\n    }\n\n    _getNewLineDefaultContext() {\n        return {\n            default_company_id: this.record.company_id,\n            default_location_id: this._defaultLocation().id,\n            default_location_dest_id: this._defaultDestLocation().id,\n            default_picking_id: this.resId,\n            default_qty_done: 1,\n            display_default_code: false,\n            hide_unlink_button: Boolean(!this.selectedLine || this.selectedLine.reserved_uom_qty),\n            force_fullfil_quantity: this.selectedLine && this.selectedLine.reserved_uom_qty,\n        };\n    }\n\n    async _cancel() {\n        await this.save();\n        await this.orm.call(\n            this.resModel,\n            'action_cancel',\n            [[this.resId]]\n        );\n        this._cancelNotification();\n        this.trigger('history-back');\n    }\n\n    _cancelNotification() {\n        this.notification(_t(\"The transfer has been cancelled\"));\n    }\n\n    _checkBarcode(barcodeData) {\n        const check = { title: _t(\"Not the expected scan\") };\n        const { location, lot, product, destLocation, packageType } = barcodeData;\n        const resultPackage = barcodeData.package;\n\n        if (this.config.restrict_scan_source_location && !barcodeData.location) {\n            // Special case where the user can not scan a destination but a source was already scanned.\n            // That means what is supposed to be a destination is in this case a source.\n            if (this.lastScanned.sourceLocation && barcodeData.destLocation &&\n                this.config.restrict_scan_dest_location == 'no') {\n                barcodeData.location = barcodeData.destLocation;\n                delete barcodeData.destLocation;\n            }\n            // Special case where the source is mandatory and the app's waiting for but none was\n            // scanned, get the previous scanned one if possible.\n            if (!this.lastScanned.sourceLocation && this._currentLocation) {\n                this.lastScanned.sourceLocation = this._currentLocation;\n            }\n        }\n\n        if (this.config.restrict_scan_source_location && !this._currentLocation && !this.selectedLine) { // Source Location.\n            if (!location) {\n                check.title = _t(\"Mandatory Source Location\");\n                check.message = _t(\n                    \"You are supposed to scan %s or another source location\",\n                    this.location.display_name\n                );\n            }\n        } else if (this._mustScanProductFirst(barcodeData)) {\n            check.message = lot ?\n                _t(\"Scan a product before scanning a tracking number\") :\n                _t(\"You must scan a product\");\n        } else if (this.config.restrict_put_in_pack == 'mandatory' && !(resultPackage || packageType) &&\n                   this.selectedLine && !this.qty_done && !this.selectedLine.result_package_id &&\n                   ((product && product.id != this.selectedLine.product_id.id) || location || destLocation)) { // Package.\n            check.message = _t(\"You must scan a package or put in pack\");\n        } else if (this.config.restrict_scan_dest_location == 'mandatory' && !this.lastScanned.destLocation) { // Destination Location.\n            if (destLocation) {\n                this.lastScanned.destLocation = destLocation;\n            } else if (product && this.selectedLine && this.selectedLine.product_id.id != product.id) {\n                // Cannot scan another product before a destination was scanned.\n                check.title = _t(\"Mandatory Destination Location\");\n                check.message = _t(\n                    \"Please scan destination location for %s before scanning other product\",\n                    this.selectedLine.product_id.display_name\n                );\n            }\n        }\n        check.error = Boolean(check.message);\n        return check;\n    }\n\n    _mustScanProductFirst(barcodeData) {\n        const { location, product } = barcodeData;\n        const packageWithQuant = ((barcodeData.package && barcodeData.package.quant_ids) || [])\n            .length;\n        return (\n            this.config.restrict_scan_product && // Restriction on product.\n            !(product || packageWithQuant || this.selectedLine) && // A product/package was scanned.\n            !(this.config.restrict_scan_source_location && location && !this.selectedLine) // Maybe the user scanned the wrong location and trying to scan the right one\n        );\n    }\n\n    async _closeValidate(ev) {\n        const record = await this.orm.read(this.resModel, [this.record.id], [\"state\"]);\n        if (record[0].state === 'done') {\n            // Checks if the picking generated a backorder. Updates the picking's data if it's the case.\n            const backorders = await this.orm.searchRead(\n                this.backorderModel,\n                this.backordersDomain,\n                [\"display_name\", \"id\"]);\n            const buttons = backorders.map(bo => {\n                const additionalContext = { active_id: bo.id };\n                return {\n                    name: bo.display_name,\n                    onClick: () => {\n                        this.action.doAction(this.actionName, { additionalContext });\n                    },\n                };\n            });\n            if (backorders.length) {\n                const phrase = backorders.length === 1 ?\n                    _t(\"Following backorder was created:\") :\n                    _t(\"Following backorders were created:\");\n                this.validateMessage = `<div>\n                    <p>${escape(this.validateMessage)}<br>${escape(phrase)}</p>\n                </div>`;\n                this.validateMessage = markup(this.validateMessage);\n            }\n            // If all is OK, displays a notification and goes back to the previous page.\n            this.notification(this.validateMessage, { type: \"success\", buttons });\n            this.trigger('history-back');\n        }\n    }\n\n    _convertDataToFieldsParams(args) {\n        const params = {\n            lot_name: args.lotName,\n            product_id: args.product,\n            qty_done: args.quantity,\n        };\n        if (args.lot) {\n            params.lot_id = args.lot;\n        }\n        if (args.package) {\n            params.package_id = args.package;\n        }\n        if (args.packaging && args.product.tracking === \"serial\" && (this.useExistingLots || this.canCreateNewLot)) {\n            params.packaging = args.packaging;\n            params.qty_done = 0;\n        }\n        if (args.resultPackage) {\n            params.result_package_id = args.resultPackage;\n        }\n        if (args.owner) {\n            params.owner_id = args.owner;\n        }\n        if (args.destLocation) {\n            params.location_dest_id = args.destLocation.id;\n        }\n        if (args.srcLocation) {\n            params.location_id = args.srcLocation;\n        }\n        return params;\n    }\n\n    _createCommandVals(line) {\n        const values = {\n            dummy_id: line.virtual_id,\n            location_id: line.location_id,\n            location_dest_id: line.location_dest_id,\n            lot_name: line.lot_name,\n            lot_id: line.lot_id,\n            package_id: line.package_id,\n            picking_id: line.picking_id,\n            picked: true,\n            product_id: line.product_id,\n            product_uom_id: line.product_uom_id,\n            owner_id: line.owner_id,\n            quantity: line.qty_done,\n            result_package_id: line.result_package_id,\n            state: 'assigned',\n        };\n        for (const [key, value] of Object.entries(values)) {\n            values[key] = this._fieldToValue(value);\n        }\n        return values;\n    }\n\n    _getMoveLineData(id){\n        const smlData = this.cache.getRecord('stock.move.line', id);\n        smlData.dummy_id = smlData.dummy_id && Number(smlData.dummy_id);\n        // Checks if this line is already in the picking's state to get back\n        // its `virtual_id` (and so, avoid to set a new `virtual_id`).\n        let prevLine = this.currentState?.lines.find(line => line.id === id);\n        if (!prevLine && smlData.dummy_id) {\n            prevLine = this.currentState?.lines.find(line => line.virtual_id === smlData.dummy_id);\n        }\n        const previousVirtualId = prevLine && prevLine.virtual_id;\n        smlData.virtual_id = smlData.dummy_id || previousVirtualId || this._uniqueVirtualId;\n        smlData.product_id = this.cache.getRecord('product.product', smlData.product_id);\n        smlData.product_uom_id = this.cache.getRecord('uom.uom', smlData.product_uom_id);\n        smlData.location_id = this.cache.getRecord('stock.location', smlData.location_id);\n        smlData.location_dest_id = this.cache.getRecord('stock.location', smlData.location_dest_id);\n        smlData.lot_id = smlData.lot_id && this.cache.getRecord('stock.lot', smlData.lot_id);\n        smlData.owner_id = smlData.owner_id && this.cache.getRecord('res.partner', smlData.owner_id);\n        smlData.package_id = smlData.package_id && this.cache.getRecord('stock.quant.package', smlData.package_id);\n        smlData.product_packaging_id = smlData.product_packaging_id && this.cache.getRecord('product.packaging', smlData.product_packaging_id);\n\n        if (this.reloadingMoveLines) {\n            if (prevLine) {\n                smlData.sortIndex = prevLine.sortIndex;\n                if (smlData.quantity && !smlData.qty_done) {\n                    // The reservation likely changed.\n                    smlData.reserved_uom_qty = smlData.quantity;\n                } else {\n                    // The reservation of this line is already known.\n                    smlData.reserved_uom_qty = prevLine.reserved_uom_qty;\n                }\n            } else {\n                // This line was created in the Barcode App, so it has no reservation.\n                smlData.qty_done = smlData.quantity;\n                smlData.reserved_uom_qty = 0;\n            }\n        } else {\n            // First loading: `reserved_uom_qty` keeps in memory what is the\n            // initial reservation for this move line clientside only, this\n            // information is lost once the user closes the operation.\n            smlData.reserved_uom_qty = smlData.quantity;\n        }\n\n        const resultPackage = smlData.result_package_id && this.cache.getRecord('stock.quant.package', smlData.result_package_id);\n        if (resultPackage) { // Fetch the package type if needed.\n            smlData.result_package_id = resultPackage;\n            const packageType = resultPackage && resultPackage.package_type_id;\n            resultPackage.package_type_id = packageType && this.cache.getRecord('stock.package.type', packageType);\n        }\n        return smlData;\n    }\n\n    _createLinesState() {\n        const lines = [];\n        const picking = this.cache.getRecord(this.resModel, this.resId);\n        for (const id of picking.move_line_ids) {\n            const smlData = this._getMoveLineData(id);\n            lines.push(smlData);\n        }\n        return lines;\n    }\n\n    _defaultLocation() {\n        return this.cache.getRecord('stock.location', this.record.location_id);\n    }\n\n    _defaultDestLocation() {\n        return this.cache.getRecord('stock.location', this.record.location_dest_id);\n    }\n\n    _getCommands() {\n        const commands = Object.assign(super._getCommands(), {\n            'OBTPRSL': this.print.bind(this, false, 'action_print_delivery_slip'),\n            'OBTPROP': this.print.bind(this, false, 'do_print_picking'),\n            \"OBTSCRA\": this._scrap.bind(this),\n            'OBTRETU': this._returnProducts.bind(this)\n        });\n        if (!this.isDone) {\n            commands['OBTPACK'] = this._putInPack.bind(this);\n            commands['OCDCANC'] = this._cancel.bind(this);\n        }\n        return commands;\n    }\n\n    _getDefaultMessageType() {\n        if (this.useScanSourceLocation && !this.lastScanned.sourceLocation) {\n            return 'scan_src';\n        }\n        return 'scan_product';\n    }\n\n    _getModelRecord() {\n        const record = this.cache.getRecord(this.resModel, this.resId);\n        if (record.picking_type_id && record.state !== \"cancel\") {\n            record.picking_type_id = this.cache.getRecord('stock.picking.type', record.picking_type_id);\n        }\n        if (record.partner_id && record.state !== \"cancel\") {\n            record.partner_id = this.cache.getRecord('res.partner', record.partner_id);\n        }\n        return record;\n    }\n\n    _getNewLineDefaultValues(fieldsParams) {\n        const defaultValues = super._getNewLineDefaultValues(...arguments);\n        if (this.selectedLine && !fieldsParams.move_id &&\n            this.selectedLine.product_id.id === fieldsParams.product_id?.id) {\n            defaultValues.move_id = this.selectedLine.move_id;\n        }\n        const newLineDefaultVals = Object.assign(defaultValues, {\n            location_dest_id: this._defaultDestLocation(),\n            reserved_uom_qty: 0,\n            qty_done: 0,\n            picking_id: this.resId,\n            result_package_id: false,\n        });\n        if (fieldsParams.product_id?.tracking === \"serial\" && fieldsParams.packaging) {\n            newLineDefaultVals.reserved_uom_qty = 1;\n        }\n        return newLineDefaultVals;\n    }\n\n    _getFieldToWrite() {\n        return [\n            'location_id',\n            'location_dest_id',\n            'lot_id',\n            'lot_name',\n            'package_id',\n            'owner_id',\n            'qty_done',\n            'result_package_id',\n        ];\n    }\n\n    _getSaveCommand() {\n        const commands = this._getSaveLineCommand();\n        if (commands.length) {\n            return {\n                route: '/stock_barcode/save_barcode_data',\n                params: {\n                    model: this.resModel,\n                    res_id: this.resId,\n                    write_field: 'move_line_ids',\n                    write_vals: commands,\n                },\n            };\n        }\n        return {};\n    }\n\n    _getScanPackageMessage() {\n        return _t(\"Scan a package or put in pack\");\n    }\n\n    _groupSublines(sublines, ids, virtual_ids, qtyDemand, qtyDone) {\n        return Object.assign(super._groupSublines(...arguments), {\n            reserved_uom_qty: qtyDemand,\n            qty_done: qtyDone,\n        });\n    }\n\n    _incrementTrackedLine() {\n        return !(this.record.use_create_lots || this.record.use_existing_lots);\n    }\n\n    _lineCannotBeTaken(line){\n        // A packed line without expected quantity or completed cannot be taken\n        const fullyPacked = line.result_package_id && (!line.reserved_uom_qty || this._lineIsComplete(line))\n        return fullyPacked || super._lineCannotBeTaken(...arguments)\n    }\n\n    _lineIsComplete(line) {\n        let isComplete = line.reserved_uom_qty && line.qty_done >= line.reserved_uom_qty;\n        if (line.isPackageLine && !line.reserved_uom_qty && line.qty_done) {\n            return true; // For package line, considers an unreserved package as a completed line.\n        }\n        if (isComplete && line.lines) { // Grouped lines/package lines have multiple sublines.\n            for (const subline of line.lines) {\n                // For tracked product, a line with `qty_done` but no tracking number is considered as not complete.\n                if (subline.product_id.tracking != 'none') {\n                    if (subline.qty_done && !(subline.lot_id || subline.lot_name)) {\n                        return false;\n                    }\n                } else if (subline.reserved_uom_qty && subline.qty_done < subline.reserved_uom_qty) {\n                    return false;\n                }\n            }\n        }\n        return isComplete;\n    }\n\n    _lineIsNotComplete(line) {\n        const currentLine = (line.product_id.tracking !== \"none\" && this._getParentLine(line)) || line;\n        const isNotComplete = currentLine.reserved_uom_qty && currentLine.qty_done < currentLine.reserved_uom_qty;\n        if (!isNotComplete && currentLine.lines) { // Grouped lines/package lines have multiple sublines.\n            for (const subline of currentLine.lines) {\n                // For tracked product, a line with `qty_done` but no tracking number is considered as not complete.\n                if (subline.product_id.tracking != 'none') {\n                    if (subline.qty_done && !(subline.lot_id || subline.lot_name)) {\n                        return true;\n                    }\n                } else if (subline.reserved_uom_qty && subline.qty_done < subline.reserved_uom_qty) {\n                    return true;\n                }\n            }\n        }\n        return isNotComplete;\n    }\n\n    _lineNeedsToBePacked(line) {\n        return Boolean(\n            this.config.lines_need_to_be_packed && line.qty_done && !line.result_package_id);\n    }\n\n    _moveEntirePackage() {\n        return this.record.picking_type_entire_packs;\n    }\n\n    async _processBarcode(barcode) {\n        if (this.isDone && !this.commands[barcode]) {\n            return this.notification(_t(\"This picking is already done\"), { type: \"danger\" });\n        }\n        return super._processBarcode(barcode);\n    }\n\n    async _processLocation(barcodeData) {\n        super._processLocation(...arguments);\n        if (barcodeData.destLocation) {\n            await this._processLocationDestination(barcodeData);\n            this.trigger('update');\n        }\n    }\n\n    async _processLocationSource(barcodeData) {\n        // For planned transfers, check the scanned location is a part of transfer source location.\n        if (this._useReservation && !this._isSublocation(barcodeData.location, this._defaultLocation())) {\n            barcodeData.stopped = true;\n            const message = _t(\"The scanned location doesn't belong to this operation's location\");\n            return this.notification(message, { type: 'danger' });\n        }\n        super._processLocationSource(...arguments);\n        // Splits uncompleted lines to be able to add reserved products from unreserved location.\n        let currentLine = this.selectedLine || this.lastScannedLine;\n        currentLine = this._getParentLine(currentLine) || currentLine;\n        if (currentLine && currentLine.location_id.id !== barcodeData.location.id){\n            const qtyDone = this.getQtyDone(currentLine);\n            const reservedQty = this.getQtyDemand(currentLine);\n            const remainingQty = reservedQty - qtyDone;\n            if (this.shouldSplitLine(currentLine)) {\n                const fieldsParams = this._convertDataToFieldsParams(barcodeData);\n                let newLine;\n                if (currentLine.lines) {\n                    for (const line of currentLine.lines) {\n                        if (!line.reserved_uom_qty) {\n                            line.reserved_uom_qty = line.qty_done;\n                        }\n                        if (this.shouldSplitLine(line) && !newLine) {\n                            newLine = await this._createNewLine({\n                                copyOf: line,\n                                fieldsParams,\n                            });\n                            delete newLine.parentLine;\n                            line.reserved_uom_qty = line.qty_done;\n                        }\n                    }\n                } else {\n                    newLine = await this._createNewLine({\n                        copyOf: currentLine,\n                        fieldsParams,\n                    });\n                }\n                currentLine.reserved_uom_qty = qtyDone;\n                if (newLine) {\n                    newLine.reserved_uom_qty = remainingQty;\n                    newLine.lot_id = false;\n                    this._markLineAsDirty(newLine);\n                }\n                this._markLineAsDirty(currentLine);\n            }\n        }\n    }\n\n    /**\n     * Returns true if the first given location is a sublocation of the second given location.\n     * @param {Object} childLocation\n     * @param {Object} parentLocation\n     * @returns {boolean}\n     */\n    _isSublocation(childLocation, parentLocation) {\n        return childLocation.parent_path.includes(parentLocation.parent_path);\n    }\n\n    _getLinesToMove() {\n        const configScanDest = this.config.restrict_scan_dest_location;\n        // Usually, assign the destination to the selected line or to the selected package's lines.\n        let lines = this.selectedPackageLine?.lines || this.selectedLine ? [this.selectedLine] : [];\n        if (configScanDest === \"mandatory\" && this.selectedLine?.product_id?.tracking !== \"none\") {\n            // When we assign the location to only the last scanned line, if the selected line is\n            // tracked, we want to assign the destination to its scanned sibling lines too.\n            const parentLine = this._getParentLine(this.selectedLine);\n            if (parentLine) {\n                lines = this.previousScannedLines.filter(\n                    line => parentLine.virtual_ids.includes(line.virtual_id)\n                );\n            }\n        } else if (configScanDest === \"optional\" && this.previousScannedLines?.length) {\n            // If config is \"After group of Products\", get all previously scanned lines.\n            for (const line of this.previousScannedLines) {\n                if (!lines.find(l => l.virtual_id === line.virtual_id)) {\n                    lines.push(line);\n                }\n            }\n        }\n        if (this.previousScannedLinesByPackage?.length) {\n            // In case some lines were added by scanning a package, get those lines.\n            lines = this.previousScannedLinesByPackage;\n        }\n\n        return Array.from(new Set(lines));\n    }\n\n    _getLineMoveId(line) {\n        return line.move_id;\n    }\n\n    _onExit() {\n        if ([\"done\", \"cancel\"].includes(this.record.state) || this.moveIds?.length === 0) {\n            // No need to all post process if operation is closed or have no move.\n            return;\n        }\n        const quantitiesByMove = this.initialState.lines.reduce((res, line) => {\n            const moveId = this._getLineMoveId(line);\n            if (res[moveId]) {\n                res[moveId].quantity_done += line.qty_done;\n                res[moveId].reserved_uom_qty += line.reserved_uom_qty;\n            } else {\n                res[moveId] = {\n                    quantity_done: line.qty_done,\n                    reserved_uom_qty: line.reserved_uom_qty,\n                };\n            }\n            return res;\n        }, {});\n        return this.orm.call(\"stock.move\", \"post_barcode_process\", [this.moveIds, quantitiesByMove]);\n    }\n\n    async _processLocationDestination(barcodeData) {\n        const configScanDest = this.config.restrict_scan_dest_location;\n        if (configScanDest == \"no\") {\n            return;\n        }\n        // For planned transfers, check the scanned location is a part of transfer destination.\n        if (this._useReservation && !this._isSublocation(barcodeData.destLocation, this._defaultDestLocation())) {\n            barcodeData.stopped = true;\n            const message = _t(\"The scanned location doesn't belong to this operation's destination\");\n            return this.notification(message, { type: 'danger' });\n        }\n\n        // Change the destination of all concerned lines.\n        const lines = this._getLinesToMove();\n        for (const line of lines) {\n            await this.changeDestinationLocation(barcodeData.destLocation.id, line);\n        }\n        barcodeData.stopped = true;\n    }\n\n    async _processPackage(barcodeData) {\n        const { packageName } = barcodeData;\n        const recPackage = barcodeData.package;\n        this.lastScanned.packageId = false;\n        if (barcodeData.packageType && !recPackage) {\n            // Scanned a package type and no existing package: make a put in pack (forced package type).\n            barcodeData.stopped = true;\n            return await this._processPackageType(barcodeData);\n        } else if (packageName && !recPackage) {\n            // Scanned a non-existing package: make a put in pack.\n            barcodeData.stopped = true;\n            return await this._putInPack({ default_name: packageName });\n        } else if (!recPackage) {\n            return; // No package, package's type or package's name => Nothing to do.\n        }\n        const packLocation = recPackage.location_id\n            ? this.cache.dbIdCache['stock.location'][recPackage.location_id]\n            : false;\n        if (recPackage.location_id && !packLocation) {\n            // The package is in a location but the location was not found in the cache,\n            // surely because this location is not related to this picking.\n            return;\n        }\n        if (packLocation && packLocation.id !== this._defaultDestLocation().id && (\n            (this.config.restrict_scan_source_location && packLocation.id !== this.location.id) ||\n            (!this.config.restrict_scan_source_location && !this._isSublocation(packLocation, this.location))\n        )) {\n            // Package is not located at the destination (result package) and is not located at the\n            // scanned source location (or one of its sublocations) neither.\n            return;\n        }\n        // If move entire package, checks if the scanned package matches a package line.\n        if (this._moveEntirePackage()) {\n            for (const packageLine of this.packageLines) {\n                if (packageLine.package_id.name !== (packageName || recPackage.name)) {\n                    continue;\n                }\n                barcodeData.stopped = true;\n                if (packageLine.qty_done) {\n                    this.lastScanned.packageId = packageLine.package_id.id;\n                    const message = _t(\"This package is already scanned.\");\n                    this.notification(message, { type: \"danger\" });\n                    return this.trigger('update');\n                }\n                for (const line of packageLine.lines) {\n                    this.selectedLineVirtualId = line.virtual_id;\n                    await this._updateLineQty(line, { qty_done: line.reserved_uom_qty });\n                    this._markLineAsDirty(line);\n                }\n                return this.trigger('update');\n            }\n        }\n        // Scanned a package: fetches package's quant and creates a line for\n        // each of them, except if the package is already scanned.\n        // TODO: can check if quants already in cache to avoid to make a RPC if\n        // there is all in it (or make the RPC only on missing quants).\n        const res = await this.orm.call(\n            'stock.quant',\n            'get_stock_barcode_data_records',\n            [recPackage.quant_ids]\n        );\n        this.cache.setCache(res.records);\n        const quants = res.records['stock.quant'];\n        // If the package is empty or is already at the destination location,\n        // assign it to the last scanned line.\n        const currentLine = this.selectedLine || this.lastScannedLine;\n        if (currentLine && (!quants.length || (\n            !currentLine.result_package_id && recPackage.location_id === currentLine.location_dest_id.id))) {\n            let linesToUpdate = [currentLine];\n            if (this.config.restrict_put_in_pack === \"optional\") {\n                linesToUpdate.push(...this.previousScannedLines.filter(line => {\n                    return line.qty_done && !line.result_package_id &&\n                           line.virtual_id !== currentLine.virtual_id;\n                }));\n            }\n            for (const line of linesToUpdate) {\n                await this._assignEmptyPackage(line, recPackage);\n            }\n            barcodeData.stopped = true;\n            this.lastScanned.packageId = recPackage.id;\n            this.trigger('update');\n            return;\n        }\n\n        if (this.location && (!packLocation || !this._isSublocation(packLocation, this.location))) {\n            // Package not at the source location: can't add its content.\n            return;\n        }\n        // Checks if the package is already scanned.\n        let alreadyExisting = 0;\n        for (const line of this.pageLines) {\n            if (line.package_id && line.package_id.id === recPackage.id &&\n                this.getQtyDone(line) > 0) {\n                alreadyExisting++;\n            }\n        }\n        if (alreadyExisting >= quants.length) {\n            barcodeData.error = _t(\"This package is already scanned.\");\n            return;\n        }\n\n        if (alreadyExisting) {\n            const userConfirmation = new Deferred();\n            this.dialogService.add(ConfirmationDialog, {\n                body: _t(\"You have already scanned %s items of this package. Do you want to scan the whole package?\", alreadyExisting),\n                title: _t(\"Scanning package\"),\n                cancel: () => userConfirmation.resolve(false),\n                confirm: () => userConfirmation.resolve(true),\n                close: () => userConfirmation.resolve(false),\n            });\n            if (!(await userConfirmation)) {\n                barcodeData.stopped = true;\n                return;\n            }\n        }\n\n        // For each quants, creates or increments a barcode line.\n        for (const quant of quants) {\n            const product = this.cache.getRecord('product.product', quant.product_id);\n            const searchLineParams = Object.assign({}, barcodeData, { product });\n            let remaining_qty = quant.quantity;\n            let qty_used = 0;\n            while (remaining_qty > 0) {\n                const currentLine = this._findLine(searchLineParams);\n                if (currentLine) { // Updates an existing line.\n                    const qty_needed = Math.max(currentLine.reserved_uom_qty - currentLine.qty_done, 0);\n                    qty_used = qty_needed ? Math.min(qty_needed, remaining_qty) : remaining_qty;\n                    const fieldsParams = this._convertDataToFieldsParams({\n                        quantity: qty_used,\n                        lotName: barcodeData.lotName,\n                        lot: barcodeData.lot,\n                        package: recPackage,\n                        owner: barcodeData.owner,\n                    });\n                    await this.updateLine(currentLine, fieldsParams);\n                } else { // Creates a new line.\n                    qty_used = remaining_qty;\n                    const fieldsParams = this._convertDataToFieldsParams({\n                        product,\n                        quantity: qty_used,\n                        lot: quant.lot_id,\n                        package: quant.package_id,\n                        resultPackage: quant.package_id,\n                        owner: quant.owner_id,\n                        srcLocation: quant.location_id,\n                    });\n                    await this._createNewLine({ fieldsParams });\n                }\n                remaining_qty -= qty_used;\n            }\n        }\n        barcodeData.stopped = true;\n        this.selectedLineVirtualId = false;\n        this.lastScanned.packageId = recPackage.id;\n        this.trigger('update');\n    }\n\n    async _processPackageType(barcodeData) {\n        const { packageType } = barcodeData;\n        const line = this.selectedLine;\n        if (!line || !line.qty_done) {\n            barcodeData.stopped = true;\n            const message = _t(\"You can't apply a package type. First, scan product or select a line\");\n            return this.notification(message, { type: \"warning\" });\n        }\n        const resultPackage = line.result_package_id;\n        if (!resultPackage) { // No package on the line => Do a put in pack.\n            const additionalContext = { default_package_type_id: packageType.id };\n            if (barcodeData.packageName) {\n                additionalContext.default_name = barcodeData.packageName;\n            }\n            await this._putInPack(additionalContext);\n        } else if (resultPackage.package_type_id.id !== packageType.id) {\n            // Changes the package type for the scanned one.\n            await this.save();\n            await this.orm.write('stock.quant.package', [resultPackage.id], {\n                package_type_id: packageType.id,\n            });\n            const message = _t(\n                \"Package type %(type)s applied to the package %(package)s\",\n                { type: packageType.name, package: resultPackage.name }\n            );\n            this.notification(message, { type: \"success\" });\n            this.trigger('refresh');\n        }\n    }\n\n    async _putInPack(additionalContext = {}) {\n        const context = Object.assign({ barcode_view: true }, additionalContext);\n        if (!this.groups.group_tracking_lot) {\n            return this.notification(\n                _t(\"To use packages, enable 'Packages' in the settings\"),\n                { type: 'danger'}\n            );\n        }\n        // Before the put in pack, create a new empty move line with the remaining\n        // quantity for each uncompleted move line who will be packaged.\n        const lines = [...this.pageLines];\n        for (const line of lines) {\n            if (line.result_package_id || !this.shouldSplitLine(line)) {\n                continue; // Line is already in a package or no quantity to process.\n            }\n            await this.splitLine(line);\n        }\n        await this.save();\n        const result = await this.orm.call(\n            this.resModel,\n            'action_put_in_pack',\n            [[this.resId]],\n            { context }\n        );\n        if (typeof result === 'object') {\n            this.trigger('process-action', result);\n        } else {\n            this.trigger('refresh');\n        }\n    }\n\n    async _returnProducts() {\n         const action = await this.orm.call(\n             this.resModel,\n             'action_create_return_picking',\n            [[this.resId]]\n         )\n        return this.action.doAction(action, { stackPosition: \"replaceCurrentAction\" });\n    }\n\n    async _scrap() {\n        if (!this.canScrap) {\n            const message = _t(\"You can't register scrap at this state of the operation\");\n            return this.notification(message, { type: \"warning\" });\n        }\n        await this.newScrapProduct();\n    }\n\n    /**\n     * Set the pickings's responsible to the active user.\n     */\n    async _setUser() {\n        if (this.record.id && this.record.user_id != user.userId) {\n            this.record.user_id = user.userId;\n            await this.orm.write(this.resModel, [this.record.id], { user_id: user.userId });\n        }\n    }\n\n    _setLocationFromBarcode(result, location) {\n        if (this.record.picking_type_code === 'outgoing') {\n            result.location = location;\n        } else if (this.record.picking_type_code === 'incoming') {\n            result.destLocation = location;\n        } else if (this.previousScannedLines.length || this.previousScannedLinesByPackage.length) {\n            if (this.config.restrict_scan_source_location && this.config.restrict_scan_dest_location === 'no'\n            && this.barcodeInfo.class != 'scan_dest') {\n                result.location = location;\n            } else {\n                result.destLocation = location;\n            }\n        } else if ([\"scan_product_or_dest\", \"scan_dest\"].includes(this.barcodeInfo.class)) {\n            result.destLocation = location;\n        } else {\n            result.location = location;\n        }\n        return result;\n    }\n\n    _sortingMethod(l1, l2) {\n        const l1IsCompleted = this._lineIsComplete(l1);\n        const l2IsCompleted = this._lineIsComplete(l2);\n        // Complete lines always on the bottom.\n        if (!l1IsCompleted && l2IsCompleted) {\n            return -1;\n        } else if (l1IsCompleted && !l2IsCompleted) {\n            return 1;\n        }\n        return super._sortingMethod(...arguments);\n    }\n\n    _updateLineQty(line, args) {\n        if (args.qty_done) {\n            if (args.uom) {\n                // An UoM was passed alongside the quantity, needs to check it's\n                // compatible with the product's UoM.\n                const lineUOM = line.product_uom_id;\n                if (args.uom.category_id !== lineUOM.category_id) {\n                    // Not the same UoM's category -> Can't be converted.\n                    const message = _t(\n                        \"Scanned quantity uses %(unit)s as its Unit of Measure (UoM), but it is not compatible with the line's UoM (%(lineUnit)s).\",\n                        { unit: args.uom.name, lineUnit: lineUOM.name }\n                    );\n                    return this.notification(message, { title: _t(\"Wrong Unit of Measure\"), type: \"danger\" });\n                } else if (args.uom.id !== lineUOM.id) {\n                    // Compatible but not the same UoM => Need a conversion.\n                    args.qty_done = (args.qty_done / args.uom.factor) * lineUOM.factor;\n                    args.uom = lineUOM;\n                }\n            }\n            if (line.product_id.tracking === 'serial') {\n                const nextQty = line.qty_done + args.qty_done;\n                if (nextQty > 1 && (this.record.use_create_lots || this.record.use_existing_lots)) {\n                    return; // Can't have more than 1 qty by serial number.\n                }\n            }\n            line.qty_done += args.qty_done;\n            this._setUser();\n        }\n    }\n\n    _updateLotName(line, lotName) {\n        line.lot_name = lotName;\n    }\n\n    async _processGs1Data(data) {\n        const result = await super._processGs1Data(...arguments);\n        const { rule } = data;\n        if (result.location && (rule.type === 'location_dest' || this.barcodeInfo.class === 'scan_product_or_dest')) {\n            result.destLocation = result.location;\n            result.location = undefined;\n        }\n        return result;\n    }\n\n    _getCompanyId() {\n        return this.record.company_id;\n    }\n}\n", "/** @odoo-module **/\n\nimport { ApplyQuantDialog } from '@stock_barcode/components/apply_quant_dialog';\nimport BarcodeModel from '@stock_barcode/models/barcode_model';\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport default class BarcodeQuantModel extends BarcodeModel {\n    constructor(params) {\n        super(...arguments);\n        this.lineModel = this.resModel;\n        this.validateMessage = _t(\"The inventory count has been updated\");\n        this.validateMethod = \"action_validate\";\n        this.deleteLineMethod = this.validateMethod;\n    }\n\n    async validate() {\n        return this.apply();\n    }\n\n    /**\n     * Check if the Inventory Adjustment can be applied and apply it only if it can be.\n     * @returns {Promise}\n     */\n    apply() {\n        if (this.checkBeforeApply()) {\n            return this._apply();\n        }\n    }\n\n    /**\n     * Makes some checks and returns true if the Inventory Adjustment can be\n     * applied or display a notification if it can not.\n     * @returns {Boolean}\n     */\n    checkBeforeApply() {\n        if (this.applyOn === 0) {\n            const message = _t(\"There is nothing to apply in this page.\");\n            this.notification(message, { type: \"warning\" });\n            return false;\n        }\n        // Checks if there are not counted serial numbers in the same location than counted quants.\n        const countedSerialNumbers = this.groupedLines.filter(\n            gl => gl.lines && gl.inventory_quantity_set && gl.product_id.tracking === \"serial\"\n        );\n        const notCountedSiblingSerialNumbers = [];\n        for (const groupedLine of countedSerialNumbers) {\n            for (const line of groupedLine.lines) {\n                if (!line.inventory_quantity_set) {\n                    notCountedSiblingSerialNumbers.push(line);\n                }\n            }\n        }\n        // In case there is not counted SN, asks the user if they want to count them as missing.\n        if (notCountedSiblingSerialNumbers.length) {\n            this.dialogService.add(ApplyQuantDialog, {\n                onApply: this._apply.bind(this),\n                onApplyAll: () => {\n                    // Set not counted SN as counted before to apply.\n                    for (const line of notCountedSiblingSerialNumbers) {\n                        this.toggleAsCounted(line);\n                    }\n                    this._apply();\n                },\n            });\n            return false;\n        }\n        return true;\n    }\n\n    /**\n     * Apply quantity set on counted quants.\n     * @returns {Promise}\n     */\n    async _apply() {\n        await this.save();\n        const linesToApply = this.pageLines.filter(line => line.inventory_quantity_set);\n        const quantIds = linesToApply.map(quant => quant.id);\n        const action = await this.orm.call(\"stock.quant\", \"action_validate\", [quantIds]);\n        const notifyAndGoAhead = res => {\n            if (res && res.special) { // Do nothing if come from a discarded wizard.\n                return this.trigger('refresh');\n            }\n            this.notification(this.validateMessage, { type: \"success\" });\n            this.trigger('history-back');\n        };\n        if (action && action.res_model) {\n            return this.action.doAction(action, { onClose: notifyAndGoAhead });\n        }\n        notifyAndGoAhead();\n    }\n\n    get isValidForBarcodeLookup() {\n        if (this.resModel === \"stock.quant\") {\n            return true;\n        }\n        return false;\n    }\n\n    get applyOn() {\n        return this.pageLines.filter(line => line.inventory_quantity_set).length;\n    }\n\n    get barcodeInfo() {\n        // Takes the parent line if the current line is part of a group.\n        let line = this._getParentLine(this.selectedLine) || this.selectedLine;\n        if (!line && this.lastScanned.packageId) {\n            line = this.pageLines.find(l => l.package_id && l.package_id.id === this.lastScanned.packageId);\n        }\n        // Defines some messages who can appear in multiple cases.\n        const messages = {\n            scanProduct: {\n                class: 'scan_product',\n                message: _t(\"Scan a product\"),\n                icon: 'tags',\n            },\n            scanLot: {\n                class: 'scan_lot',\n                message: _t(\n                    \"Scan lot numbers for product %s to change their quantity\",\n                    line ? line.product_id.display_name : \"\"\n                ),\n                icon: 'barcode',\n            },\n            scanSerial: {\n                class: 'scan_serial',\n                message: _t(\n                    \"Scan serial numbers for product %s to change their quantity\",\n                    line ? line.product_id.display_name : \"\"\n                ),\n                icon: 'barcode',\n            },\n        };\n\n        if (line) { // Message depends of the selected line's state.\n            const { tracking } = line.product_id;\n            const trackingNumber = this.getlotName(line);\n            if (this._lineIsNotComplete(line)) {\n                if (tracking !== 'none') {\n                    return tracking === 'lot' ? messages.scanLot : messages.scanSerial;\n                }\n                return messages.scanProduct;\n            } else if (tracking !== 'none' && !trackingNumber) {\n                // Line's quantity is fulfilled but still waiting a tracking number.\n                return tracking === 'lot' ? messages.scanLot : messages.scanSerial;\n            } else { // Line's quantity is fulfilled.\n                if (this.groups.group_stock_multi_locations && line.location_id.id === this.location.id) {\n                    return {\n                        class: 'scan_product_or_src',\n                        message: _t(\n                            \"Scan more products in %s or scan another location\",\n                            this.location.display_name\n                        ),\n                    };\n                }\n                return messages.scanProduct;\n            }\n        }\n        // No line selected, returns default message (depends if multilocation is enabled).\n        if (this.groups.group_stock_multi_locations) {\n            if (!this.lastScanned.sourceLocation) {\n                return {\n                    class: 'scan_src',\n                    message: _t(\"Scan a location\"),\n                    icon: 'sign-out',\n                };\n            }\n            return {\n                class: 'scan_product_or_src',\n                message: _t(\n                    \"Scan a product in %s or scan another location\",\n                    this.location.display_name\n                ),\n            };\n        }\n        return messages.scanProduct;\n    }\n\n    get displayByUnitButton () {\n        return true;\n    }\n\n    displaySetButton(line) {\n        const isSelected = this.selectedLineVirtualId === line.virtual_id;\n        return isSelected && (this.showQuantityCount || (\n            line.product_id.tracking === \"serial\" && this.getlotName(line)));\n    }\n\n    setData(data) {\n        this.userId = data.data.user_id;\n        this.showQuantityCount = data.data.show_quantity_count;\n        this.countEntireLocation = data.data.count_entire_location;\n        super.setData(...arguments);\n        const companies = data.data.records['res.company'];\n        this.companyIds = companies.map(company => company.id);\n        this.lineFormViewId = data.data.line_view_id;\n    }\n\n    get displayApplyButton() {\n        return true;\n    }\n\n    getQtyDone(line) {\n        return line.inventory_quantity_set ? line.inventory_quantity : 0;\n    }\n\n    getQtyDemand(line) {\n        return this.showQuantityCount ? line.quantity : 0;\n    }\n\n    getActionRefresh(newId) {\n        const action = super.getActionRefresh(newId);\n        action.params.res_id = this.currentState.lines.map(l => l.id);\n        if (newId) {\n            action.params.res_id.push(newId);\n        }\n        return action;\n    }\n\n    get highlightValidateButton() {\n        return this.applyOn > 0 && this.applyOn === this.pageLines.length;\n    }\n\n    IsNotSet(line) {\n        return !line.inventory_quantity_set;\n    }\n\n    lineCanBeDeleted(line) {\n        return line.inventory_quantity_set && this.getQtyDone(line) === 0;\n    }\n\n    lineIsFaulty(line) {\n        if (this.showQuantityCount) {\n            return line.inventory_quantity_set && line.inventory_quantity !== line.quantity;\n        }\n        return false; // Never show a line as faulty if we don't display the expected quantity.\n    }\n\n    lineIsTracked(line) {\n        const lineIsTracked = super.lineIsTracked(...arguments);\n        if (lineIsTracked && line.product_id.tracking === \"serial\") {\n            // Count quants tracked by SN as untracked if they have no SN and multiple quantity.\n            return this.getlotName(line) || (this.getQtyDone(line) <= 1 && this.getQtyDemand(line) <= 1);\n        }\n        return lineIsTracked\n    }\n\n    get printButtons() {\n        return [{\n            name: _t(\"Print Inventory\"),\n            class: 'o_print_inventory',\n            action: 'stock.action_report_inventory',\n        }];\n    }\n\n    get recordIds() {\n        return this.currentState.lines.map(l => l.id);\n    }\n\n    /**\n     * Marks or unmarks the line as counted and set its inventory quantity to zero.\n     *\n     * @param {Object} line\n     */\n    toggleAsCounted(line) {\n        line.inventory_quantity = 0;\n        line.inventory_quantity_set = !line.inventory_quantity_set;\n        this._markLineAsDirty(line);\n        this.trigger('update');\n    }\n\n    updateLineQty(virtualId, qty = 1) {\n        this.actionMutex.exec(() => {\n            const line = this.pageLines.find(l => l.virtual_id === virtualId);\n            this.updateLine(line, {inventory_quantity: qty});\n            this.trigger('update');\n        });\n    }\n\n    // --------------------------------------------------------------------------\n    // Private\n    // --------------------------------------------------------------------------\n\n    _getCommands() {\n        return Object.assign(super._getCommands(), {\n            'OBTAPPLY': this.apply.bind(this),\n        });\n    }\n\n    _getMissingRecordsParams() {\n        const params = super._getMissingRecordsParams();\n        params.fetch_quants = true;\n        return params;\n    }\n\n    _getNewLineDefaultContext() {\n        return {\n            default_company_id: this.companyIds[0],\n            default_location_id: this._defaultLocation().id,\n            default_inventory_quantity: 1,\n            default_user_id: this.userId,\n            inventory_mode: true,\n            display_default_code: false,\n            hide_qty_to_count: !this.showQuantityCount,\n        };\n    }\n\n    _createCommandVals(line) {\n        const values = {\n            dummy_id: line.virtual_id,\n            inventory_date: line.inventory_date,\n            inventory_quantity: line.inventory_quantity,\n            inventory_quantity_set: line.inventory_quantity_set,\n            location_id: line.location_id,\n            lot_id: line.lot_id,\n            lot_name: line.lot_name,\n            package_id: line.package_id,\n            product_id: line.product_id,\n            owner_id: line.owner_id,\n            user_id: this.userId,\n        };\n        for (const [key, value] of Object.entries(values)) {\n            values[key] = this._fieldToValue(value);\n        }\n        return values;\n    }\n\n    async _createNewLine(params) {\n        // When creating a new line, we need to know if a quant already exists\n        // for this line, and in this case, update the new line fields.\n        const product = params.fieldsParams.product_id;\n        if (! product.is_storable) {\n            const productName = (product.default_code ? `[${product.default_code}] ` : '') + product.display_name;\n            const message = _t(\n                \"%s can't be inventoried. Only storable products can be inventoried.\",\n                productName\n            );\n            this.notification(message, { type: \"warning\" });\n            return false;\n        }\n        const location_id = this.location.id;\n        const { lot_id, lot_name, owner_id, package_id } = params.fieldsParams;\n        let quants = [];\n        if (!params.fieldsParams.packaging || product.tracking === \"none\") {\n            if (!lot_id && !lot_name && !package_id && !owner_id) {\n                quants = await this.cache.getQuants(product, location_id);\n            } else {\n                const quantParams = { lot_id, lot_name, owner_id, package_id };\n                quants = await this.cache.getQuants(product, location_id, quantParams);\n            }\n        }\n        if (quants.length === 1 && (\n            product.tracking === 'none' || params.fieldsParams.lot_name || params.fieldsParams.lot_id)) {\n            const inventory_quantity = product.tracking === \"lot\"\n                ? quants[0].quantity\n                : params.fieldsParams.inventory_quantity || 1;\n            params.fieldsParams = Object.assign({}, params.fieldsParams, { inventory_quantity });\n        }\n        let newLine = false;\n        if (quants.length) { // Found existing quants: create a line for each one.\n            const lineIds = this.currentState.lines.map(l => l.id);\n            for (const quant of quants) {\n                if (lineIds.includes(quant.id)) {\n                    continue; // Don't create line for quant if there is already a line for it.\n                }\n                const lineParams = {\n                    fieldsParams: Object.assign({}, quant, params.fieldsParams),\n                };\n                const newlyCreatedLine = await super._createNewLine(lineParams);\n                this.selectedLineVirtualId = newlyCreatedLine.virtual_id;\n                // Keeps the first created line so that the one who will be selected.\n                newLine = newLine || newlyCreatedLine;\n                // If the quant already exits, we add it into the `initialState` to\n                // avoid comparison issue with the `currentState` when the save occurs.\n                const lineWithOriginalQuantValues = Object.assign({}, newlyCreatedLine, {\n                    inventory_date: quant.inventory_date,\n                    inventory_quantity: quant.inventory_quantity,\n                    inventory_quantity_set: quant.inventory_quantity_set,\n                    quantity: quant.quantity,\n                    user_id: quant.user_id,\n                });\n                this.initialState.lines.push(lineWithOriginalQuantValues);\n            }\n        } else { // No existing quant: creates an empty new line.\n            newLine = await super._createNewLine(params);\n        }\n        return newLine;\n    }\n\n    _convertDataToFieldsParams(args) {\n        const params = {};\n        // Set the fields in `params` only if they are in `args`.\n        if (args.packaging && args.product.tracking === 'serial') {\n            params.inventory_quantity = 1;\n        } else if (args.quantity) {\n            params.inventory_quantity = args.quantity;\n        }\n        args.lot && (params.lot_id = args.lot);\n        args.lotName && (params.lot_name = args.lotName);\n        args.owner && (params.owner_id = args.owner);\n        args.package && (params.package_id = args.package);\n        args.product && (params.product_id = args.product);\n        args.product && args.product.uom_id && (params.product_uom_id = args.product.uom_id);\n        args.packaging && (params.packaging = args.packaging);\n        return params;\n    }\n\n    _getNewLineDefaultValues(fieldsParams) {\n        const defaultValues = super._getNewLineDefaultValues(...arguments);\n        Object.assign(defaultValues, {\n            inventory_date: new Date().toISOString().slice(0, 10),\n            inventory_quantity: 0,\n            quantity: (fieldsParams && fieldsParams.quantity) || 0,\n            user_id: this.userId,\n        });\n        // Marks the new line's quantity as set only if it's not an existing quant (no `quantity`)\n        // or if it already has a counted quantity. It's to avoid tragedy if the user applies by\n        // mistake the inventory adjustment after scanned a product with multiple serial/lot numbers\n        if (fieldsParams.quantity === undefined || fieldsParams.inventory_quantity) {\n            defaultValues.inventory_quantity_set = true;\n        }\n        return defaultValues\n    }\n\n    _getFieldToWrite() {\n        return [\n            'inventory_date',\n            'inventory_quantity',\n            'inventory_quantity_set',\n            'user_id',\n            'location_id',\n            'lot_name',\n            'lot_id',\n            'package_id',\n            'owner_id',\n        ];\n    }\n\n    _getSaveCommand() {\n        const commands = this._getSaveLineCommand();\n        if (commands.length) {\n            return {\n                route: '/stock_barcode/save_barcode_data',\n                params: {\n                    model: this.resModel,\n                    res_id: false,\n                    write_field: false,\n                    write_vals: commands,\n                },\n            };\n        }\n        return {};\n    }\n\n    _groupSublines(sublines, ids, virtual_ids, qtyDemand, qtyDone) {\n        const hasAtLeastOneSetSubline = sublines.find(l => l.inventory_quantity_set);\n        return Object.assign(super._groupSublines(...arguments), {\n            inventory_quantity: qtyDone,\n            quantity: qtyDemand,\n            inventory_quantity_set: hasAtLeastOneSetSubline,\n        });\n    }\n\n    _lineIsNotComplete(line) {\n        return line.inventory_quantity === 0;\n    }\n\n    async _processPackage(barcodeData) {\n        const { packageType, packageName } = barcodeData;\n        let recPackage = barcodeData.package;\n        this.lastScanned.packageId = false;\n        if (!recPackage && !packageType && !packageName) {\n            return; // No Package data to process.\n        }\n        // Scan a new package and/or a package type -> Create a new package with those parameters.\n        const currentLine = this.selectedLine || this.lastScannedLine;\n        if (currentLine.package_id && packageType &&\n            !recPackage && ! packageName &&\n            currentLine.package_id.id !== packageType) {\n            // Changes the package type for the scanned one.\n            await this.orm.write('stock.quant.package', [currentLine.package_id.id], {\n                package_type_id: packageType.id,\n            });\n            const message = _t(\"Package type %(type)s applied to the package %(package)s\", {\n                type: packageType.name,\n                package: currentLine.package_id.name,\n            });\n            barcodeData.stopped = true;\n            return this.notification(message, { type: \"success\" });\n        }\n        if (!recPackage) {\n            if (currentLine && !currentLine.package_id) {\n                const valueList = {};\n                if (packageName) {\n                    valueList.name = packageName;\n                }\n                if (packageType) {\n                    valueList.package_type_id = packageType.id;\n                }\n                const newPackageData = await this.orm.call(\n                    'stock.quant.package',\n                    'action_create_from_barcode',\n                    [valueList]\n                );\n                this.cache.setCache(newPackageData);\n                recPackage = newPackageData['stock.quant.package'][0];\n            }\n        }\n        if (!recPackage && packageName) {\n            const currentLine = this.selectedLine || this.lastScannedLine;\n            if (currentLine && !currentLine.package_id) {\n                const newPackageData = await this.orm.call(\n                    'stock.quant.package',\n                    'action_create_from_barcode',\n                    [{ name: packageName }]\n                );\n                this.cache.setCache(newPackageData);\n                recPackage = newPackageData['stock.quant.package'][0];\n            }\n        }\n        if (!recPackage || (\n            recPackage.location_id && recPackage.location_id != this.location.id\n        )) {\n            return;\n        }\n        // TODO: can check if quants already in cache to avoid to make a RPC if\n        // there is all in it (or make the RPC only on missing quants).\n        const res = await this.orm.call(\n            'stock.quant',\n            'get_stock_barcode_data_records',\n            [recPackage.quant_ids]\n        );\n        const quants = res.records['stock.quant'];\n        if (!quants.length) { // Empty package => Assigns it to the last scanned line.\n            const currentLine = this.selectedLine || this.lastScannedLine;\n            if (currentLine && !currentLine.package_id) {\n                const fieldsParams = this._convertDataToFieldsParams({\n                    package: recPackage,\n                });\n                await this.updateLine(currentLine, fieldsParams);\n                barcodeData.stopped = true;\n                this.selectedLineVirtualId = false;\n                this.lastScanned.packageId = recPackage.id;\n                this.trigger('update');\n            }\n            return;\n        }\n        this.cache.setCache(res.records);\n\n        // Checks if the package is already scanned.\n        let alreadyExisting = 0;\n        for (const line of this.pageLines) {\n            if (line.package_id && line.package_id.id === recPackage.id &&\n                this.getQtyDone(line) > 0) {\n                alreadyExisting++;\n            }\n        }\n        if (alreadyExisting === quants.length) {\n            barcodeData.error = _t(\"This package is already scanned.\");\n            return;\n        }\n        // For each quants, creates or increments a barcode line.\n        for (const quant of quants) {\n            const product = this.cache.getRecord('product.product', quant.product_id);\n            const searchLineParams = Object.assign({}, barcodeData, { product });\n            const currentLine = this._findLine(searchLineParams);\n            if (currentLine) { // Updates an existing line.\n                const fieldsParams = this._convertDataToFieldsParams({\n                    quantity: quant.quantity,\n                    lotName: barcodeData.lotName,\n                    lot: barcodeData.lot,\n                    package: recPackage,\n                    owner: barcodeData.owner,\n                });\n                await this.updateLine(currentLine, fieldsParams);\n            } else { // Creates a new line.\n                const fieldsParams = this._convertDataToFieldsParams({\n                    product,\n                    quantity: quant.quantity,\n                    lot: quant.lot_id,\n                    package: quant.package_id,\n                    owner: quant.owner_id,\n                });\n                const newLine = await this._createNewLine({ fieldsParams });\n                newLine.inventory_quantity = quant.quantity;\n            }\n        }\n        barcodeData.stopped = true;\n        this.selectedLineVirtualId = false;\n        this.lastScanned.packageId = recPackage.id;\n        this.trigger('update');\n    }\n\n    async _processLocation(barcodeData) {\n        super._processLocation(barcodeData)\n        if (barcodeData.location && this.countEntireLocation) {\n            await this.loadQuantsForLocation(barcodeData);\n        }\n    }\n\n    async loadQuantsForLocation(barcodeData) {\n        const res = await this.orm.call(\n            \"stock.location\",\n            \"get_counted_quant_data_records\",\n            [barcodeData.location.id]\n        );\n        this.cache.setCache(res.records);\n\n        const quants = res.records['stock.quant'];\n        for (const quant of quants) {\n            const product = this.cache.getRecord('product.product', quant.product_id);\n            const lot = quant.lot_id && this.cache.getRecord('stock.lot', quant.lot_id);\n            const searchLineParams = Object.assign({}, barcodeData, { product, lot });\n            const currentLine = this._findLine(searchLineParams);\n            if (!currentLine) {\n                const fieldsParams = this._convertDataToFieldsParams({\n                    product,\n                    quantity: quant.quantity,\n                    lot: quant.lot_id,\n                    package: quant.package_id,\n                    resultPackage: quant.package_id,\n                    owner: quant.owner_id,\n                });\n                const newLine = await this._createNewLine({ fieldsParams });\n                if (newLine) {\n                    newLine.inventory_quantity = quant.inventory_quantity;\n                    newLine.inventory_quantity_set = false;\n                }\n            }\n        }\n        barcodeData.stopped = true;\n        this.selectedLineVirtualId = false;\n        this.trigger('update');\n    }\n\n    _updateLineQty(line, args) {\n        if (args.quantity) { // Set stock quantity.\n            line.quantity = args.quantity;\n        }\n        if (args.inventory_quantity) { // Increments inventory quantity.\n            if (args.uom) {\n                // An UoM was passed alongside the quantity, needs to check it's\n                // compatible with the product's UoM.\n                const productUOM = this.cache.getRecord('uom.uom', line.product_id.uom_id);\n                if (args.uom.category_id !== productUOM.category_id) {\n                    // Not the same UoM's category -> Can't be converted.\n                    const message = _t(\n                        \"Scanned quantity uses %(unit)s as its Unit of Measure (UoM), but it is not compatible with the product's UoM (%(productUnit)s).\",\n                        { unit: args.uom.name, productUnit: productUOM.name }\n                    );\n                    return this.notification(message, { title: _t(\"Wrong Unit of Measure\"), type: \"warning\" });\n                } else if (args.uom.id !== productUOM.id) {\n                    // Compatible but not the same UoM => Need a conversion.\n                    args.inventory_quantity = (args.inventory_quantity / args.uom.factor) * productUOM.factor;\n                }\n            }\n            line.inventory_quantity += args.inventory_quantity;\n            if (line.inventory_quantity > 0) {\n                args.inventory_quantity_set = true;\n            }\n            line.inventory_quantity_set = this.countEntireLocation ? args.inventory_quantity_set : true;\n            if (line.product_id.tracking === 'serial' && (line.lot_name || line.lot_id)) {\n                line.inventory_quantity = Math.max(0, Math.min(1, line.inventory_quantity));\n            }\n        }\n    }\n\n    async _updateLotName(line, lotName) {\n        if (line.lot_name === lotName) {\n            // No need to update the line's tracking number if it's already set.\n            return Promise.resolve();\n        }\n        line.lot_name = lotName;\n        const owner_id = line.owner_id ? line.owner_id : false;\n        const package_id = line.package_id && line.package_id;\n        const existingQuant = await this.cache.getQuants(line.product_id, line.location_id.id, {\n            lot_id: line.lot_id,\n            lot_name: lotName,\n            owner_id,\n            package_id,\n        });\n        if (existingQuant && existingQuant.length) {\n            Object.assign(line, existingQuant[0]);\n            if (line.lot_id) {\n                line.lot_id = await this.cache.getRecordByBarcode(lotName, \"stock.lot\");\n            }\n        }\n    }\n\n    _canOverrideTrackingNumber(line, newLotName) {\n        return super._canOverrideTrackingNumber(...arguments) && (!line.id || line.lot_id);\n    }\n\n    _createLinesState() {\n        const today = new Date().toISOString().slice(0, 10);\n        const lines = [];\n        for (const id of Object.keys(this.cache.dbIdCache['stock.quant']).map(id => Number(id))) {\n            const quant = this.cache.getRecord('stock.quant', id);\n            if (quant.user_id !== this.userId || quant.inventory_date > today) {\n                // Doesn't take quants who must be counted by another user or in the future.\n                continue;\n            }\n            // Checks if this line is already in the quant state to get back\n            // its `virtual_id` (and so, avoid to set a new `virtual_id`).\n            const prevLine = this.currentState && this.currentState.lines.find(l => l.id === id);\n            const previousVirtualId = prevLine && prevLine.virtual_id;\n            quant.dummy_id = quant.dummy_id && Number(quant.dummy_id);\n            quant.virtual_id = quant.dummy_id || previousVirtualId || this._uniqueVirtualId;\n            quant.product_id = this.cache.getRecord('product.product', quant.product_id);\n            quant.product_uom_id = this.cache.getRecord('uom.uom', quant.product_uom_id);\n            quant.location_id = this.cache.getRecord('stock.location', quant.location_id);\n            quant.lot_id = quant.lot_id && this.cache.getRecord('stock.lot', quant.lot_id);\n            quant.package_id = quant.package_id && this.cache.getRecord('stock.quant.package', quant.package_id);\n            quant.owner_id = quant.owner_id && this.cache.getRecord('res.partner', quant.owner_id);\n            lines.push(Object.assign({}, quant));\n        }\n        return lines;\n    }\n\n    _getName() {\n        return _t(\"Inventory Count\");\n    }\n\n    _getPrintOptions() {\n        const options = super._getPrintOptions();\n        const quantsToPrint = this.pageLines.filter(quant => quant.inventory_quantity_set);\n        if (quantsToPrint.length === 0) {\n            return { warning: _t(\"There is nothing to print in this page.\") };\n        }\n        options.additionalContext = { active_ids: quantsToPrint.map(quant => quant.id) };\n        return options;\n    }\n\n    _selectLine(line) {\n        if (this.selectedLineVirtualId !== line.virtual_id) {\n            // Unfolds the group where the line is, folds other lines' group.\n            this.unfoldLineKey = this.groupKey(line);\n        }\n        super._selectLine(...arguments);\n    }\n\n    zeroQtyClass(line) {\n        return this.IsNotSet(line) ? super.zeroQtyClass(...arguments) : \"text-danger\";\n    }\n\n    _getCompanyId() {\n        return this.companyIds[0];\n    }\n}\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { useRecordObserver } from \"@web/model/relational_model/utils\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, onWillStart, useState } from \"@odoo/owl\";\n\nexport class Digipad extends Component {\n    static template = \"stock_barcode.DigipadTemplate\";\n    static props = {\n        ...standardWidgetProps,\n        fieldToEdit: { type: String },\n        fulfilledAt: { type: String, optional: true },\n    };\n\n    setup() {\n        this.orm = useService('orm');\n        const { data } = this.props.record;\n        const context = this.props.record.evalContext.context;\n        this.quantity = data[this.props.fieldToEdit];\n        this.value = String(this.quantity);\n        this.fulfillQuantity = this.props.fulfilledAt && !context.hide_qty_to_count\n            ? data[this.props.fulfilledAt]\n            : 0;\n        if (context.force_fullfil_quantity) {\n            this.fulfillQuantity = context.force_fullfil_quantity;\n        }\n        const field = this.props.record.model.config.fields[this.props.fieldToEdit];\n        this.precision = field.digits[1];\n        this.productId = this.props.record.data.product_id[0];\n        this.state = useState({\n            packagingButtons: [],\n        });\n        useRecordObserver(async (record) => {\n            if (this.productId != record.data.product_id[0]) {\n                this.productId = record.data.product_id[0];\n                await this._fetchPackagingButtons();\n            }\n        });\n        onWillStart(async () => {\n            this.displayUOM = await user.hasGroup('uom.group_uom');\n            await this._fetchPackagingButtons();\n        });\n    }\n\n    get changes() {\n        return { [this.props.fieldToEdit]: Number(this.value) };\n    }\n\n    get quantityToFulfill() {\n        if (!this.fulfillQuantity) {\n            return 0;\n        }\n        const record = this.props.record.data;\n        const currentQty = record[this.props.fieldToEdit];\n        return this.fulfillQuantity - currentQty;\n    }\n\n    get buttonContainerClass() {\n        return this.fulfillQuantity ? 'col-3' : 'col-4';\n    }\n\n    get buttonFulfillClass() {\n        if (this.quantityToFulfill > 0) {\n            return \"btn-success\";\n        } else if (this.quantityToFulfill < 0) {\n            return \"btn-warning\";\n        }\n        return \"btn-secondary text-success\";\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Copies the input value if digipad value is not set yet, or overrides it if there is a\n     * difference between the two values (in case user has manualy edited the input value).\n     * @private\n     */\n    _checkInputValue() {\n        const input = document.querySelector(`div[name=\"${this.props.fieldToEdit}\"] input`);\n        const inputValue = input.value;\n        if (Number(this.value) != Number(inputValue)) {\n            this.value = inputValue;\n            this.quantity = Number(this.value || 0);\n        }\n    }\n\n    /**\n     * Increments the field value by the interval amount (1 by default).\n     * @private\n     * @param {integer} [interval=1]\n     */\n    async _increment(interval=1, enforceQuantity=false) {\n        if (enforceQuantity) {\n            this.quantity = interval;\n        } else {\n            this._checkInputValue();\n            this.quantity = Math.max(this.quantity + interval, 0);\n        }\n        this.value = this.quantity.toFixed(this.precision);\n        if (parseFloat(this.value) % 1 == 0) {\n            this.value = String(Math.floor(parseFloat(this.value)));\n        }\n        await this.props.record.update(this.changes);\n    }\n\n    /**\n     * Search for the product's packaging buttons.\n     * @private\n     * @returns {Promise}\n     */\n    async _fetchPackagingButtons() {\n        const record = this.props.record.data;\n        if (record.product_id[0]) {\n            const domain = [[\"product_id\", \"=\", record.product_id[0]]];\n            if (this.quantityToFulfill) { // Doesn't fetch packaging with a too high quantity.\n                domain.push([\"qty\", \"<=\", this.quantityToFulfill]);\n            }\n            this.state.packagingButtons = await this.orm.searchRead(\n                \"product.packaging\",\n                domain,\n                [\"name\", \"product_uom_id\", \"qty\"],\n                { limit: 2 }\n            );\n        }\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Handles the click on one of the digipad's button and updates the value..\n     * @private\n     * @param {String} button\n     */\n    erase() {\n        this._checkInputValue();\n        this.quantity = 0;\n        this.value = String(this.quantity);\n        this.props.record.update(this.changes);\n    }\n\n    increment() {\n        this._increment();\n    }\n\n    decrement() {\n        this._increment(-1);\n    }\n\n    /**\n     * Handles the click on one of the digipad's button and updates the value..\n     * @private\n     * @param {String} button\n     */\n    fulfill() {\n        this._checkInputValue();\n        this.quantity = this.fulfillQuantity;\n        this.value = String(this.quantity);\n        this.props.record.update(this.changes);\n    }\n}\n\nexport const digipad = {\n    component: Digipad,\n    extractProps: ({ attrs }) => {\n        return {\n            fieldToEdit: attrs.field_to_edit,\n            fulfilledAt: attrs.fulfilled_at,\n        };\n    },\n};\nregistry.category('view_widgets').add('digipad', digipad);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { ImageField } from \"@web/views/fields/image/image_field\";\nimport { Component } from \"@odoo/owl\";\n\nexport class FullScreenImage extends Component {\n    static template = \"stock_barcode.FullScreenImage\";\n    static props = {\n        src: {type: String},\n        close: Function,\n    };\n}\n\nexport class ImagePreviewField extends ImageField {\n    static template = \"stock_barcode.ImagePreviewField\";\n\n    setup() {\n        super.setup();\n        this.dialog = useService(\"dialog\");\n    }\n\n    openImageFullScreen() {\n        this.dialog.add(FullScreenImage, {\n            src: this.getUrl(this.props.name),\n        });\n    }\n}\n\nexport const imageClickEnlarge = {\n    component: ImagePreviewField,\n};\n\nregistry.category(\"fields\").add(\"image_preview\", imageClickEnlarge);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\nimport { user } from \"@web/core/user\";\nimport { Component, onWillStart } from \"@odoo/owl\";\n\nexport class SetReservedQuantityButton extends Component {\n    static props = {\n        ...standardFieldProps,\n        fieldToSet: { type: String },\n    };\n    static template = \"stock_barcode.SetReservedQuantityButtonTemplate\";\n\n    setup() {\n        onWillStart(async () => {\n            this.displayUOM = await user.hasGroup('uom.group_uom');\n        });\n    }\n\n    get uom() {\n        const [id, name] = this.props.record.data.product_uom_id || [];\n        return { id, name };\n    }\n\n    _setQuantity (ev) {\n        ev.stopPropagation();\n        this.props.record.update({ [this.props.fieldToSet]: this.props.record.data[this.props.name] });\n    }\n}\n\nexport const setReservedQuantityButton = {\n    component: SetReservedQuantityButton,\n    extractProps: ({ attrs }) => {\n        if (attrs.field_to_set) {\n            return { fieldToSet: attrs.field_to_set };\n        }\n        return {};\n    },\n};\n\nregistry.category(\"fields\").add(\"set_reserved_qty_button\", setReservedQuantityButton);\n", "/** @odoo-module */\n\nimport { X2ManyField, x2ManyField } from \"@web/views/fields/x2many/x2many_field\";\nimport {\n    useX2ManyCrud,\n    useOpenX2ManyRecord,\n} from \"@web/views/fields/relational_utils\";\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\nimport { CommonSkillsListRenderer } from \"../../views/skills_list_renderer\";\nimport { useService } from '@web/core/utils/hooks';\nimport { onWillStart } from \"@odoo/owl\";\n\n\nexport class SkillsListRenderer extends CommonSkillsListRenderer {\n    static template = \"hr_skills.SkillsListRenderer\";\n    setup() {\n        super.setup();\n        this.orm = useService('orm');\n        this.actionService = useService(\"action\");\n\n        onWillStart(async () => {\n            const res = await this.orm.searchCount('hr.skill.type', []);\n            this.anySkills = res > 0;\n            [this.user] = await this.orm.read(\"res.users\", [user.userId], [\"employee_ids\"]);\n            this.IsHrUser = await user.hasGroup(\"hr.group_hr_user\");\n            this.userSubordinates = (await this.orm.searchRead(\n                \"hr.employee\",\n                [[\"id\", \"child_of\", this.user.employee_ids]],\n                [\"id\"]\n            )).map((record) => record[\"id\"]);\n        });\n    }\n\n    get groupBy() {\n        return 'skill_type_id';\n    }\n\n    async skillTypesAction() {\n        return this.actionService.doAction(\"hr_skills.hr_skill_type_action\");\n    }\n\n    async openSkillsReport() {\n        // fetch id through employee or public.employee\n        const id = this.env.model.root.data.id || this.env.model.root.data.employee_id[0];\n-        this.actionService.doAction({\n            type: \"ir.actions.act_window\",\n            name: _t(\"Skills Report\"),\n            res_model: \"hr.employee.skill.log\",\n            view_mode: \"graph,list\",\n            views: [[false, \"graph\"], [false, \"list\"]],\n            context: {\n                'fill_temporal': 0,\n            },\n            target: \"current\",\n            domain: [['employee_id', '=', id]],\n        });\n    }\n\n    get showTimeline() {\n        return this.SkillsRight && !this.props.list.context.no_timeline;\n    }\n\n    get SkillsRight() {\n        let isSubordinate = false;\n        if (this.env.model.root.data.employee_id) {\n            isSubordinate = this.userSubordinates.includes(this.env.model.root.data.employee_id[0]);\n        }\n        return this.IsHrUser || isSubordinate;\n    }\n}\n\nexport class SkillsX2ManyField extends X2ManyField {\n    static components = {\n        ...X2ManyField.components,\n        ListRenderer: SkillsListRenderer,\n    };\n    setup() {\n        super.setup()\n        const { saveRecord, updateRecord } = useX2ManyCrud(\n            () => this.list,\n            this.isMany2Many\n        );\n\n        const openRecord = useOpenX2ManyRecord({\n            resModel: this.list.resModel,\n            activeField: this.activeField,\n            activeActions: this.activeActions,\n            getList: () => this.list,\n            saveRecord: async (record) => {\n                await saveRecord(record);\n                await this.props.record.save();\n            },\n            updateRecord: updateRecord,\n            withParentId: this.props.widget !== \"many2many\",\n        });\n\n        this._openRecord = (params) => {\n            params.title = this.getWizardTitleName();\n            openRecord({...params});\n        };\n    }\n\n    getWizardTitleName() {\n        return _t(\"Select Skills\")\n    }\n\n    async onAdd({ context, editable } = {}) {\n        const employeeId = this.props.record.resModel === \"res.users\" ? this.props.record.data.employee_id[0] : this.props.record.resId;\n        return super.onAdd({\n            editable,\n            context: {\n                ...context,\n                default_employee_id: employeeId,\n            }\n        });\n    }\n}\n\nexport const skillsX2ManyField = {\n    ...x2ManyField,\n    component: SkillsX2ManyField,\n};\n\nregistry.category(\"fields\").add(\"skills_one2many\", skillsX2ManyField);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { X2ManyField, x2ManyField } from \"@web/views/fields/x2many/x2many_field\";\n\n\nexport class AutoSaveSkillTypeField extends X2ManyField {\n     async onAdd({ context, editable } = {}) {\n        await this.props.record.model.root.save();\n        await super.onAdd({ context, editable });\n     }\n}\n\nexport const autoSaveSkillTypeField = {\n    ...x2ManyField,\n    component: AutoSaveSkillTypeField,\n};\n\nregistry.category(\"fields\").add(\"auto_save_skill_type\", autoSaveSkillTypeField);\n", "/** @odoo-module */\n\nimport { registry } from '@web/core/registry';\nimport { ListBooleanToggleField, listBooleanToggleField } from \"@web/views/fields/boolean_toggle/list_boolean_toggle_field\";\n\nexport class ListBooleanToggleLoadField extends ListBooleanToggleField {\n    async onChange(value) {\n        await super.onChange(value);\n        await this.props.record.model.root.save();\n        return this.env.model.load();\n    }\n}\n\nexport const listBooleanToggleLoadField = {\n    ...listBooleanToggleField,\n    component: ListBooleanToggleLoadField,\n};\n\nregistry.category(\"fields\").add(\"boolean_toggle_load\", listBooleanToggleLoadField);\n", "/** @odoo-module **/\n\nimport { TagsList } from \"@web/core/tags_list/tags_list\";\n\n\nexport class SkillsTagList extends TagsList {\n    static template = \"hr_skills.SkillsTagsList\";\n\n    getTextStyle(tag) {\n        return tag.defaultLevel\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport {\n    Many2ManyTagsField,\n    many2ManyTagsField,\n} from \"@web/views/fields/many2many_tags/many2many_tags_field\";\n\nimport { SkillsTagList } from \"../hr_skills_tags_list/hr_skills_tags_list\";\n\n\nclass SkillsMany2ManyTags extends Many2ManyTagsField {\n    static components = { ...Many2ManyTagsField.components, TagsList: SkillsTagList };\n    getTagProps(record) {\n        return { ...super.getTagProps(record), defaultLevel: record.data.default_level };\n    }\n}\n\nexport const skillsMany2ManyTags = {\n    ...many2ManyTagsField,\n    component: SkillsMany2ManyTags,\n    relatedFields: (fieldInfo) => {\n        return [\n            ...many2ManyTagsField.relatedFields(fieldInfo),\n            { name: \"default_level\", type: \"boolean\"},\n        ];\n    },\n};\n\nregistry.category(\"fields\").add(\"many2many_tags_skills\", skillsMany2ManyTags);\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { onMounted, onPatched, useRef } from \"@odoo/owl\";\n\nimport { formatDate } from \"@web/core/l10n/dates\";\n\nimport { SkillsX2ManyField, skillsX2ManyField } from \"../skills_one2many/skills_one2many\";\nimport { CommonSkillsListRenderer } from \"../../views/skills_list_renderer\";\n\nexport class ResumeListRenderer extends CommonSkillsListRenderer {\n    static template = \"hr_skills.ResumeListRenderer\";\n    static rowsTemplate = \"hr_skills.ResumeListRenderer.Rows\";\n    static recordRowTemplate = \"hr_skills.ResumeListRenderer.RecordRow\";\n    static useMagicColumnWidths = false;\n    setup() {\n        super.setup();\n\n        this.linkRef = useRef(\"link-target-blank\");\n        onMounted(this._setLinksToOpenInNewTab);\n        onPatched(this._setLinksToOpenInNewTab);\n    }\n    get groupBy() {\n        return \"line_type_id\";\n    }\n\n    get colspan() {\n        if (this.props.activeActions) {\n            return 3;\n        }\n        return 2;\n    }\n\n    formatDate(date) {\n        return formatDate(date);\n    }\n\n    _setLinksToOpenInNewTab() {\n        const resumeLines = this.linkRef.el;\n\n        // Find all links within the resume description and set target to \"_blank\"\n        if (resumeLines){\n            const links = resumeLines.querySelectorAll('a');\n\n            links.forEach(link => {\n                link.setAttribute('target', '_blank'); // Set target=\"_blank\" to open links in new tab\n            });\n        }\n    }\n}\n\nexport class ResumeX2ManyField extends SkillsX2ManyField {\n    static components = {\n        ...SkillsX2ManyField.components,\n        ListRenderer: ResumeListRenderer,\n    };\n    getWizardTitleName() {\n        return _t(\"New Resume line\");\n    }\n}\n\nexport const resumeX2ManyField = {\n    ...skillsX2ManyField,\n    component: ResumeX2ManyField,\n};\n\nregistry.category(\"fields\").add(\"resume_one2many\", resumeX2ManyField);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\n\nexport class CommonSkillsListRenderer extends ListRenderer {\n    get colspan() {\n        const span = this.allColumns.length;\n        if (this.isEditable) {\n            return span + 1;\n        }\n\n        return span;\n    }\n\n    get groupBy() {\n        return '';\n    }\n\n    get groupedList() {\n        const grouped = {};\n\n        for (const record of this.list.records) {\n            const data = record.data;\n            const group = data[this.groupBy];\n\n            if (grouped[group[1]] === undefined) {\n                grouped[group[1]] = {\n                    id: parseInt(group[0]),\n                    name: group[1] || _t('Other'),\n                    list: {\n                        records: [],\n                    },\n                };\n            }\n\n            grouped[group[1]].list.records.push(record);\n        }\n        return grouped;\n    }\n\n    get showTable() {\n        return this.props.list.records.length;\n    }\n\n    get isEditable() {\n        return this.props.editable !== false;\n    }\n\n    async onCellClicked(record, column, ev) {\n        if (!this.isEditable) {\n            return;\n        }\n\n        return await super.onCellClicked(record, column, ev);\n    }\n}\nCommonSkillsListRenderer.rowsTemplate = \"hr_skills.SkillsListRenderer.Rows\";\n", "/** @odoo-module **/\n\nimport { patch } from \"@web/core/utils/patch\";\nimport { AvatarCardResourcePopover } from \"@resource_mail/components/avatar_card_resource/avatar_card_resource_popover\";\n\n\nexport const patchAvatarCardResourcePopover = {\n    loadAdditionalData() {\n        const promises = super.loadAdditionalData();\n        this.skills = false;\n        if (this.record.employee_skill_ids?.length) {\n            promises.push(\n                this.orm\n                    .read(\"hr.employee.skill\", this.record.employee_skill_ids, [\"display_name\", \"color\"])\n                    .then((skills) => {\n                        this.skills = skills;\n                    })\n            );\n        }\n        return promises;\n    },\n    get fieldNames() {\n        return [\n            ...super.fieldNames,\n            \"employee_skill_ids\",\n        ];\n    },\n    get skillTags() {\n        return this.skills.map(({ id, display_name, color }) => ({\n            id,\n            text: display_name,\n            colorIndex: color,\n        }));\n    },\n};\n\nexport const unpatchAvatarCardResourcePopover = patch(AvatarCardResourcePopover.prototype, patchAvatarCardResourcePopover);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from '@web/core/registry';\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport {\n    BooleanToggleField,\n    booleanToggleField,\n} from \"@web/views/fields/boolean_toggle/boolean_toggle_field\";\n\nexport class ConfirmCheckBox extends CheckBox {\n    onClick(ev) {\n        ev.preventDefault();\n\n        if (ev.target.tagName !== \"INPUT\") {\n            return;\n        }\n        this.props.onChange(ev.target.checked);\n    }\n}\n\nexport class BooleanToggleConfirm extends BooleanToggleField {\n    static template = \"stock_account.BooleanToggleConfirm\";\n    static components = { ConfirmCheckBox };\n\n    setup() {\n        super.setup();\n        this.dialogService = useService('dialog');\n    }\n\n    onChange(value) {\n        const record = this.props.record.data;\n        const updateAndSave = () => {\n            this.props.record.update({ [this.props.name]: value }, { save: true });\n        };\n\n        if (record.lot_valuated && !value) {\n            this.dialogService.add(ConfirmationDialog, {\n                body: _t(\"This operation might lead in a loss of data. Valuation will be identical for all lots/SN. Do you want to proceed ? \"),\n                confirm: updateAndSave,\n                cancel: () => {},\n            });\n\n        }\n        else {\n            updateAndSave();\n        }\n    }\n}\n\nexport const booleanToggleConfirm = {\n    ...booleanToggleField,\n    component: BooleanToggleConfirm,\n};\n\nregistry.category(\"fields\").add(\"confirm_boolean\", booleanToggleConfirm);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\n\nimport { ForecastedHeader as Parent } from \"@stock/stock_forecasted/forecasted_header\";\n\nexport class StockAccountForecastedHeader extends Parent {\n    static template = \"stock_account.ForecastedHeader\";\n}\n\npatch(Parent.prototype, {\n    async _onClickValuation() {\n        const context = this._getActionContext();\n        return this.action.doAction({\n            name: _t('Stock Valuation'),\n            res_model: 'stock.valuation.layer',\n            type: 'ir.actions.act_window',\n            view_mode: 'list,form',\n            views: [[false, 'list'], [false, 'form']],\n            target: 'current',\n            context: context,\n        });\n    }\n});\n\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { ControlPanel } from \"@web/search/control_panel/control_panel\";\n\nimport { Component, onWillStart, useState, useSubEnv } from \"@odoo/owl\";\n\nimport { AccountReportController } from \"@account_reports/components/account_report/controller\";\nimport { AccountReportButtonsBar } from \"@account_reports/components/account_report/buttons_bar/buttons_bar\";\nimport { AccountReportCogMenu } from \"@account_reports/components/account_report/cog_menu/cog_menu\";\nimport { AccountReportEllipsis } from \"@account_reports/components/account_report/ellipsis/ellipsis\";\nimport { AccountReportFilters } from \"@account_reports/components/account_report/filters/filters\";\nimport { AccountReportHeader } from \"@account_reports/components/account_report/header/header\";\nimport { AccountReportLine } from \"@account_reports/components/account_report/line/line\";\nimport { AccountReportLineCell } from \"@account_reports/components/account_report/line_cell/line_cell\";\nimport { AccountReportLineName } from \"@account_reports/components/account_report/line_name/line_name\";\nimport { AccountReportSearchBar } from \"@account_reports/components/account_report/search_bar/search_bar\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\nimport { useSetupAction } from \"@web/search/action_hook\";\n\n\nexport class AccountReport extends Component {\n    static template = \"account_reports.AccountReport\";\n    static props = { ...standardActionServiceProps };\n    static components = {\n        ControlPanel,\n        AccountReportButtonsBar,\n        AccountReportCogMenu,\n        AccountReportSearchBar,\n    };\n\n    static customizableComponents = [\n        AccountReportEllipsis,\n        AccountReportFilters,\n        AccountReportHeader,\n        AccountReportLine,\n        AccountReportLineCell,\n        AccountReportLineName,\n    ];\n    static defaultComponentsMap = [];\n\n    setup() {\n\n        useSetupAction({\n            getLocalState: () => {\n                return {\n                    keep_journal_groups_options: true,  // used when using the breadcrumb\n                };\n            }\n        })\n        if (this.props?.state?.keep_journal_groups_options !== undefined) {\n            this.props.action.keep_journal_groups_options = true;\n        }\n\n        // Can not use 'control-panel-bottom-right' slot without this, as viewSwitcherEntries doesn't exist here.\n        this.env.config.viewSwitcherEntries = [];\n\n        this.orm = useService(\"orm\");\n        this.actionService = useService(\"action\");\n        this.controller = useState(new AccountReportController(this.props.action));\n        this.initialQuery = this.props.action.context?.default_filter_accounts;\n\n        for (const customizableComponent of AccountReport.customizableComponents)\n            AccountReport.defaultComponentsMap[customizableComponent.name] = customizableComponent;\n\n        onWillStart(async () => {\n            await this.controller.load(this.env);\n        });\n\n        useSubEnv({\n            controller: this.controller,\n            component: this.getComponent.bind(this),\n            template: this.getTemplate.bind(this),\n        });\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Custom overrides\n    // -----------------------------------------------------------------------------------------------------------------\n    static registerCustomComponent(customComponent) {\n        registry.category(\"account_reports_custom_components\").add(customComponent.template, customComponent);\n    }\n\n    get cssCustomClass() {\n        return this.controller.data.custom_display.css_custom_class || \"\";\n    }\n\n    getComponent(name) {\n        const customComponents = this.controller.data.custom_display.components;\n\n        if (customComponents && customComponents[name])\n            return registry.category(\"account_reports_custom_components\").get(customComponents[name]);\n\n        return AccountReport.defaultComponentsMap[name];\n    }\n\n    getTemplate(name) {\n        const customTemplates = this.controller.data.custom_display.templates;\n\n        if (customTemplates && customTemplates[name])\n            return customTemplates[name];\n\n        return `account_reports.${ name }Customizable`;\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Table\n    // -----------------------------------------------------------------------------------------------------------------\n    get tableClasses() {\n        let classes = \"\";\n\n        if (this.controller.options.columns.length > 1) {\n            classes += \" striped\";\n        }\n\n        if (this.controller.options['horizontal_split'])\n            classes += \" w-50 mx-2\";\n\n        return classes;\n    }\n}\n\nregistry.category(\"actions\").add(\"account_report\", AccountReport);\n", "/** @odoo-module */\n\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class AccountReportButtonsBar extends Component {\n    static template = \"account_reports.AccountReportButtonsBar\";\n    static props = {};\n\n    setup() {\n        this.controller = useState(this.env.controller);\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Buttons\n    //------------------------------------------------------------------------------------------------------------------\n    get barButtons() {\n        const buttons = [];\n\n        for (const button of this.controller.buttons) {\n            if (button.always_show) {\n                buttons.push(button);\n            }\n        }\n\n        return buttons;\n    }\n}\n", "/** @odoo-module **/\n\nimport {Component, useState} from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\n\n\nexport class AccountReportCogMenu extends Component {\n    static template = \"account_reports.AccountReportCogMenu\";\n    static components = {Dropdown, DropdownItem};\n    static props = {};\n\n    setup() {\n        this.controller = useState(this.env.controller);\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Buttons\n    //------------------------------------------------------------------------------------------------------------------\n    get cogButtons() {\n        const buttons = [];\n\n        for (const button of this.controller.buttons) {\n            if (!button.always_show) {\n                buttons.push(button);\n            }\n        }\n\n        return buttons;\n    }\n}\n", "/* global owl:readonly */\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { session } from \"@web/session\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { removeTaxGroupingFromLineId } from \"@account_reports/js/util\";\n\nexport class AccountReportController {\n    constructor(action) {\n        this.action = action;\n        this.actionService = useService(\"action\");\n        this.dialog = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n    }\n\n    async load(env) {\n        this.env = env;\n        this.reportOptionsMap = {};\n        this.reportInformationMap = {};\n        this.lastOpenedSectionByReport = {};\n        this.loadingCallNumberByCacheKey = new Proxy(\n            {},\n            {\n                get(target, name) {\n                    return name in target ? target[name] : 0;\n                },\n                set(target, name, newValue) {\n                    target[name] = newValue;\n                    return true;\n                },\n            }\n        );\n        this.actionReportId = this.action.context.report_id;\n        const isOpeningReport = !this.action?.keep_journal_groups_options  // true when opening the report, except when coming from the breadcrumb\n        const mainReportOptions = await this.loadReportOptions(this.actionReportId, false, this.action.params?.ignore_session, isOpeningReport);\n        const cacheKey = this.getCacheKey(mainReportOptions['sections_source_id'], mainReportOptions['report_id']);\n\n        // We need the options to be set and saved in order for the loading to work properly\n        this.options = mainReportOptions;\n        this.reportOptionsMap[cacheKey] = mainReportOptions;\n        this.incrementCallNumber(cacheKey);\n        this.options[\"loading_call_number\"] = this.loadingCallNumberByCacheKey[cacheKey];\n        this.saveSessionOptions(mainReportOptions);\n\n        const activeSectionPromise = this.displayReport(mainReportOptions['report_id']);\n        this.preLoadClosedSections();\n        await activeSectionPromise;\n    }\n\n    getCacheKey(sectionsSourceId, reportId) {\n        return `${sectionsSourceId}_${reportId}`\n    }\n\n    incrementCallNumber(cacheKey = null) {\n        if (!cacheKey) {\n            cacheKey = this.getCacheKey(this.options['sections_source_id'], this.options['report_id']);\n        }\n        this.loadingCallNumberByCacheKey[cacheKey] += 1;\n    }\n\n    async displayReport(reportId) {\n        const cacheKey = await this.loadReport(reportId);\n        const options = await this.reportOptionsMap[cacheKey];\n        const informationMap = await this.reportInformationMap[cacheKey];\n        if (\n            options !== undefined\n            && this.loadingCallNumberByCacheKey[cacheKey] === options[\"loading_call_number\"]\n            && (this.lastOpenedSectionByReport === {} || this.lastOpenedSectionByReport[options['selected_variant_id']] === options['selected_section_id'])\n        ) {\n            // the options gotten from the python correspond to the ones that called this displayReport\n            this.options = options;\n\n            // informationMap might be undefined if the promise has been deleted by another call.\n            // Don't need to set data, the call that deleted it is coming to re-put data\n            if (informationMap !== undefined) {\n                this.data = informationMap;\n                // If there is a specific order for lines in the options, we want to use it by default\n                if (this.areLinesOrdered()) {\n                    await this.sortLines();\n                }\n                this.setLineVisibility(this.lines);\n                this.refreshVisibleAnnotations();\n                this.saveSessionOptions(this.options);\n            }\n\n        }\n    }\n\n    async reload(optionPath, newOptions) {\n        const rootOptionKey = optionPath ? optionPath.split(\".\")[0] : \"\";\n\n        /*\n        When reloading the UI after setting an option filter, invalidate the cached options and data of all sections supporting this filter.\n        This way, those sections will be reloaded (either synchronously when the user tries to access them or asynchronously via the preloading\n        feature), and will then use the new filter value. This ensures the filters are always applied consistently to all sections.\n        */\n        for (const [cacheKey, cachedOptionsPromise] of Object.entries(this.reportOptionsMap)) {\n            let cachedOptions = await cachedOptionsPromise;\n\n            if (rootOptionKey === \"\" || cachedOptions.hasOwnProperty(rootOptionKey)) {\n                delete this.reportOptionsMap[cacheKey];\n                delete this.reportInformationMap[cacheKey];\n            }\n        }\n\n        this.saveSessionOptions(newOptions); // The new options will be loaded from the session. Saving them now ensures the new filter is taken into account.\n        await this.displayReport(newOptions['report_id']);\n    }\n\n    async preLoadClosedSections() {\n        let sectionLoaded = false;\n        for (const section of this.options['sections']) {\n            // Preload the first non-loaded section we find amongst this report's sections.\n            const cacheKey = this.getCacheKey(this.options['sections_source_id'], section.id);\n            if (section.id != this.options['report_id'] && !this.reportInformationMap[cacheKey]) {\n                await this.loadReport(section.id, true);\n\n                sectionLoaded = true;\n                // Stop iterating and schedule next call. We don't go on in the loop in case the cache is reset and we need to restart preloading.\n                break;\n            }\n        }\n\n        let nextCallDelay = (sectionLoaded) ? 100 : 1000;\n\n        const self = this;\n        setTimeout(() => self.preLoadClosedSections(), nextCallDelay);\n    }\n\n    async loadReport(reportId, preloading=false) {\n        const options = await this.loadReportOptions(reportId, preloading, false); // This also sets the promise in the cache\n        const reportToDisplayId = options['report_id']; // Might be different from reportId, in case the report to open uses sections\n\n        const cacheKey = this.getCacheKey(options['sections_source_id'], reportToDisplayId)\n        if (!this.reportInformationMap[cacheKey]) {\n            this.reportInformationMap[cacheKey] = this.orm.call(\n                \"account.report\",\n                options.readonly_query ? \"get_report_information_readonly\" : \"get_report_information\",\n                [\n                    reportToDisplayId,\n                    options,\n                ],\n                {\n                    context: this.action.context,\n                },\n            );\n        }\n\n        await this.reportInformationMap[cacheKey];\n\n        if (!preloading) {\n            if (options['sections'].length)\n                this.lastOpenedSectionByReport[options['sections_source_id']] = options['selected_section_id'];\n        }\n\n        return cacheKey;\n    }\n\n    async loadReportOptions(reportId, preloading=false, ignore_session=false, isOpeningReport=false) {\n        const loadOptions = (ignore_session || !this.hasSessionOptions()) ? (this.action.params?.options || {}) : this.sessionOptions();\n        const cacheKey = this.getCacheKey(loadOptions['sections_source_id'] || reportId, reportId);\n\n        if (!(cacheKey in this.loadingCallNumberByCacheKey)) {\n            this.incrementCallNumber(cacheKey);\n        }\n        loadOptions[\"loading_call_number\"] = this.loadingCallNumberByCacheKey[cacheKey];\n\n        loadOptions[\"is_opening_report\"] = isOpeningReport;\n\n        if (!this.reportOptionsMap[cacheKey]) {\n            // The options for this section are not loaded nor loading. Let's load them !\n\n            if (preloading)\n                loadOptions['selected_section_id'] = reportId;\n            else {\n                /* Reopen the last opened section by default (cannot be done through regular caching, because composite reports' options are not\n                cached (since they always reroute). */\n                if (this.lastOpenedSectionByReport[reportId])\n                    loadOptions['selected_section_id'] = this.lastOpenedSectionByReport[reportId];\n            }\n\n            this.reportOptionsMap[cacheKey] = this.orm.call(\n                \"account.report\",\n                \"get_options\",\n                [\n                   reportId,\n                   loadOptions,\n                ],\n                {\n                   context: this.action.context,\n                },\n            );\n\n            // Wait for the result, and check the report hasn't been rerouted to a section or variant; fix the cache if it has\n            let reportOptions = await this.reportOptionsMap[cacheKey];\n\n            // In case of a reroute, also set the cached options into the reroute target's key\n            const loadedOptionsCacheKey = this.getCacheKey(reportOptions['sections_source_id'], reportOptions['report_id']);\n            if (loadedOptionsCacheKey !== cacheKey) {\n                /* We delete the rerouting report from the cache, to avoid redoing this reroute when reloading the cached options, as it would mean\n                route reports can never be opened directly if they open some variant by default.*/\n                delete this.reportOptionsMap[cacheKey];\n                this.reportOptionsMap[loadedOptionsCacheKey] = reportOptions;\n\n                this.loadingCallNumberByCacheKey[loadedOptionsCacheKey] = 1;\n                delete this.loadingCallNumberByCacheKey[cacheKey];\n                return reportOptions;\n            }\n        }\n\n        return this.reportOptionsMap[cacheKey];\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Generic data getters\n    //------------------------------------------------------------------------------------------------------------------\n    get buttons() {\n        return this.options.buttons;\n    }\n\n    get caretOptions() {\n        return this.data.caret_options;\n    }\n\n    get columnHeadersRenderData() {\n        return this.data.column_headers_render_data;\n    }\n\n    get columnGroupsTotals() {\n        return this.data.column_groups_totals;\n    }\n\n    get context() {\n        return this.data.context;\n    }\n\n    get filters() {\n        return this.data.filters;\n    }\n\n    get annotations() {\n        return this.data.annotations;\n    }\n\n    get groups() {\n        return this.data.groups;\n    }\n\n    get lines() {\n        return this.data.lines;\n    }\n\n    get warnings() {\n        return this.data.warnings;\n    }\n\n    get linesOrder() {\n        return this.data.lines_order;\n    }\n\n    get report() {\n        return this.data.report;\n    }\n\n    get visibleAnnotations() {\n        return this.data.visible_annotations;\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Generic data setters\n    //------------------------------------------------------------------------------------------------------------------\n    set annotations(value) {\n        this.data.annotations = value;\n    }\n\n    set columnGroupsTotals(value) {\n        this.data.column_groups_totals = value;\n    }\n\n    set lines(value) {\n        this.data.lines = value;\n        this.setLineVisibility(this.lines);\n    }\n\n    set linesOrder(value) {\n        this.data.lines_order = value;\n    }\n\n    set visibleAnnotations(value) {\n        this.data.visible_annotations = value;\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Helpers\n    //------------------------------------------------------------------------------------------------------------------\n    get needsColumnPercentComparison() {\n        return this.options.column_percent_comparison === \"growth\";\n    }\n\n    get hasCustomSubheaders() {\n        return this.columnHeadersRenderData.custom_subheaders.length > 0;\n    }\n\n    get hasDebugColumn() {\n        return Boolean(this.options.show_debug_column);\n    }\n\n    get hasStringDate() {\n        return \"date\" in this.options && \"string\" in this.options.date;\n    }\n\n    get hasVisibleAnnotations() {\n        return Boolean(this.visibleAnnotations.length);\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Options\n    //------------------------------------------------------------------------------------------------------------------\n    async _updateOption(operationType, optionPath, optionValue=null, reloadUI=false) {\n        const optionKeys = optionPath.split(\".\");\n\n        let currentOptionKey = null;\n        let option = this.options;\n\n        while (optionKeys.length > 1) {\n            currentOptionKey = optionKeys.shift();\n            option = option[currentOptionKey];\n\n            if (option === undefined)\n                throw new Error(`Invalid option key in _updateOption(): ${ currentOptionKey } (${ optionPath })`);\n        }\n\n        switch (operationType) {\n            case \"update\":\n                option[optionKeys[0]] = optionValue;\n                break;\n            case \"delete\":\n                delete option[optionKeys[0]];\n                break;\n            case \"toggle\":\n                option[optionKeys[0]] = !option[optionKeys[0]];\n                break;\n            default:\n                throw new Error(`Invalid operation type in _updateOption(): ${ operationType }`);\n        }\n\n        if (reloadUI) {\n            this.incrementCallNumber();\n            await this.reload(optionPath, this.options);\n        }\n    }\n\n    async updateOption(optionPath, optionValue, reloadUI=false) {\n        await this._updateOption('update', optionPath, optionValue, reloadUI);\n    }\n\n    async deleteOption(optionPath, reloadUI=false) {\n        await this._updateOption('delete', optionPath, null, reloadUI);\n    }\n\n    async toggleOption(optionPath, reloadUI=false) {\n        await this._updateOption('toggle', optionPath, null, reloadUI);\n    }\n\n    async switchToSection(reportId) {\n        this.saveSessionOptions({...this.options, 'selected_section_id': reportId});\n        this.displayReport(reportId);\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Session options\n    //------------------------------------------------------------------------------------------------------------------\n    sessionOptionsID() {\n        /* Options are stored by action report (so, the report that was targetted by the original action triggering this flow).\n        This allows a more intelligent reloading of the previous options during user navigation (especially concerning sections and variants;\n        you expect your report to open by default the same section as last time you opened it in this http session).\n        */\n        return `account.report:${ this.actionReportId }:${ session.user_companies.current_company }`;\n    }\n\n    hasSessionOptions() {\n        return Boolean(browser.sessionStorage.getItem(this.sessionOptionsID()))\n    }\n\n    saveSessionOptions(options) {\n        browser.sessionStorage.setItem(this.sessionOptionsID(), JSON.stringify(options));\n    }\n\n    sessionOptions() {\n        return JSON.parse(browser.sessionStorage.getItem(this.sessionOptionsID()));\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Lines\n    //------------------------------------------------------------------------------------------------------------------\n    lineHasDebugData(lineIndex) {\n        return 'debug_popup_data' in this.lines[lineIndex];\n    }\n\n    lineHasGrowthComparisonData(lineIndex) {\n        return Boolean(this.lines[lineIndex].column_percent_comparison_data);\n    }\n\n    isLineAncestorOf(ancestorLineId, lineId) {\n        return lineId.startsWith(`${ancestorLineId}|`);\n    }\n\n    isLineChildOf(childLineId, lineId) {\n        return childLineId.startsWith(`${lineId}|`);\n    }\n\n    isLineRelatedTo(relatedLineId, lineId) {\n        return this.isLineAncestorOf(relatedLineId, lineId) || this.isLineChildOf(relatedLineId, lineId);\n    }\n\n    isNextLineChild(index, lineId) {\n        return index < this.lines.length && this.lines[index].id.startsWith(`${lineId}|`);\n    }\n\n    isNextLineDirectChild(index, lineId) {\n        return index < this.lines.length && this.lines[index].parent_id === lineId;\n    }\n\n    isTotalLine(lineIndex) {\n        return this.lines[lineIndex].id.includes(\"|total~~\");\n    }\n\n    isLoadMoreLine(lineIndex) {\n        return this.lines[lineIndex].id.includes(\"|load_more~~\");\n    }\n\n    isLoadedLine(lineIndex) {\n        const lineID = this.lines[lineIndex].id;\n        const nextLineIndex = lineIndex + 1;\n\n        return this.isNextLineChild(nextLineIndex, lineID) && !this.isTotalLine(nextLineIndex) && !this.isLoadMoreLine(nextLineIndex);\n    }\n\n    async replaceLineWith(replaceIndex, newLines) {\n        await this.insertLines(replaceIndex, 1, newLines);\n    }\n\n    async insertLinesAfter(insertIndex, newLines) {\n        await this.insertLines(insertIndex + 1, 0, newLines);\n    }\n\n    async insertLines(lineIndex, deleteCount, newLines) {\n        this.lines.splice(lineIndex, deleteCount, ...newLines);\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Unfolded/Folded lines\n    //------------------------------------------------------------------------------------------------------------------\n    async unfoldLoadedLine(lineIndex) {\n        const lineId = this.lines[lineIndex].id;\n        let nextLineIndex = lineIndex + 1;\n\n        while (this.isNextLineChild(nextLineIndex, lineId)) {\n            if (this.isNextLineDirectChild(nextLineIndex, lineId)) {\n                const nextLine = this.lines[nextLineIndex];\n                nextLine.visible = true;\n                if (!nextLine.unfoldable && this.isNextLineChild(nextLineIndex + 1, nextLine.id)) {\n                    await this.unfoldLine(nextLineIndex);\n                }\n            }\n            nextLineIndex += 1;\n        }\n        return nextLineIndex;\n    }\n\n    async unfoldNewLine(lineIndex) {\n        const options = await this.options;\n        const newLines = await this.orm.call(\n            \"account.report\",\n            options.readonly_query ? \"get_expanded_lines_readonly\" : \"get_expanded_lines\",\n            [\n                this.options['report_id'],\n                this.options,\n                this.lines[lineIndex].id,\n                this.lines[lineIndex].groupby,\n                this.lines[lineIndex].expand_function,\n                this.lines[lineIndex].progress,\n                0,\n                this.lines[lineIndex].horizontal_split_side,\n            ],\n        );\n\n        if (this.areLinesOrdered()) {\n            this.updateLinesOrderIndexes(lineIndex, newLines, false)\n        }\n        this.insertLinesAfter(lineIndex, newLines);\n\n        const totalIndex = lineIndex + newLines.length + 1;\n\n        if (this.filters.show_totals && this.lines[totalIndex] && this.isTotalLine(totalIndex))\n            this.lines[totalIndex].visible = true;\n\n        // Update options\n        this.options.unfolded_lines.push(\n            ...newLines.filter(line => line.unfolded).map(({ id }) => id)\n        );\n\n        this.saveSessionOptions(this.options);\n\n        return totalIndex\n    }\n\n    /**\n     * When unfolding a line of a sorted report, we need to update the linesOrder array by adding the new lines,\n     * which will require subsequent updates on the array.\n     *\n     * - lineOrderValue represents the line index before sorting the report.\n     * @param {Integer} lineIndex: Index of the current line\n     * @param {Array} newLines: Array of lines to be added\n     * @param {Boolean} replaceLine: Useful for the splice of the linesOrder array in case we want to replace some line\n     *                               example: With the load more, we want to replace the line with others\n     **/\n    updateLinesOrderIndexes(lineIndex, newLines, replaceLine) {\n        let unfoldedLineIndex;\n        // The offset is useful because in case we use 'replaceLineWith' we want to replace the line at index\n        // unfoldedLineIndex with the new lines.\n        const offset = replaceLine ? 0 : 1;\n        for (const [lineOrderIndex, lineOrderValue] of Object.entries(this.linesOrder)) {\n            // Since we will have to add new lines into the linesOrder array, we have to update the index of the lines\n            // having a bigger index than the one we will unfold.\n            // deleteCount of 1 means that a line need to be replaced so the index need to be increase by 1 less than usual\n            if (lineOrderValue > lineIndex) {\n                this.linesOrder[lineOrderIndex] += newLines.length - replaceLine;\n            }\n            // The unfolded line is found, providing a reference for adding children in the 'linesOrder' array.\n            if (lineOrderValue === lineIndex) {\n                unfoldedLineIndex = parseInt(lineOrderIndex)\n            }\n        }\n\n        const arrayOfNewIndex = Array.from({ length: newLines.length }, (dummy, index) => this.linesOrder[unfoldedLineIndex] + index + offset);\n        this.linesOrder.splice(unfoldedLineIndex + offset, replaceLine, ...arrayOfNewIndex);\n    }\n\n    async unfoldLine(lineIndex) {\n        const targetLine = this.lines[lineIndex];\n        let lastLineIndex = lineIndex + 1;\n\n        if (this.isLoadedLine(lineIndex))\n            lastLineIndex = await this.unfoldLoadedLine(lineIndex);\n        else if (targetLine.expand_function) {\n            lastLineIndex = await this.unfoldNewLine(lineIndex);\n        }\n\n        this.setLineVisibility(this.lines.slice(lineIndex + 1, lastLineIndex));\n        targetLine.unfolded = true;\n        this.refreshVisibleAnnotations();\n\n        // Update options\n        if (!this.options.unfolded_lines.includes(targetLine.id))\n            this.options.unfolded_lines.push(targetLine.id);\n\n        this.saveSessionOptions(this.options);\n    }\n\n    foldLine(lineIndex) {\n        const targetLine = this.lines[lineIndex];\n\n        let foldedLinesIDs = new Set([targetLine.id]);\n        let nextLineIndex = lineIndex + 1;\n\n        while (this.isNextLineChild(nextLineIndex, targetLine.id)) {\n            this.lines[nextLineIndex].unfolded = false;\n            this.lines[nextLineIndex].visible = false;\n\n            foldedLinesIDs.add(this.lines[nextLineIndex].id);\n\n            nextLineIndex += 1;\n        }\n\n        targetLine.unfolded = false;\n\n        this.refreshVisibleAnnotations();\n\n        // Update options\n        this.options.unfolded_lines = this.options.unfolded_lines.filter(\n            unfoldedLineID => !foldedLinesIDs.has(unfoldedLineID)\n        );\n\n        this.saveSessionOptions(this.options);\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Ordered lines\n    //------------------------------------------------------------------------------------------------------------------\n    linesCurrentOrderByColumn(columnIndex) {\n        if (this.areLinesOrderedByColumn(columnIndex))\n            return this.options.order_column.direction;\n\n        return \"default\";\n    }\n\n    areLinesOrdered() {\n        return this.linesOrder != null && this.options.order_column != null;\n    }\n\n    areLinesOrderedByColumn(columnIndex) {\n        return this.areLinesOrdered() && this.options.order_column.expression_label === this.options.columns[columnIndex].expression_label;\n    }\n\n    async sortLinesByColumnAsc(columnIndex) {\n        this.options.order_column = {\n            expression_label: this.options.columns[columnIndex].expression_label,\n            direction: \"ASC\",\n        };\n\n        await this.sortLines();\n        this.saveSessionOptions(this.options);\n    }\n\n    async sortLinesByColumnDesc(columnIndex) {\n        this.options.order_column = {\n            expression_label: this.options.columns[columnIndex].expression_label,\n            direction: \"DESC\",\n        };\n\n        await this.sortLines();\n        this.saveSessionOptions(this.options);\n    }\n\n    sortLinesByDefault() {\n        delete this.options.order_column;\n        delete this.data.lines_order;\n\n        this.saveSessionOptions(this.options);\n    }\n\n    async sortLines() {\n        this.linesOrder = await this.orm.call(\n            \"account.report\",\n            \"sort_lines\",\n            [\n                this.lines,\n                this.options,\n                true,\n            ],\n            {\n                context: this.action.context,\n            },\n        );\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Annotations\n    //------------------------------------------------------------------------------------------------------------------\n    async refreshAnnotations() {\n        this.annotations = await this.orm.call(\"account.report\", \"get_annotations\", [\n            this.action.context.report_id,\n            this.options,\n        ]);\n\n        this.refreshVisibleAnnotations();\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Visibility\n    //------------------------------------------------------------------------------------------------------------------\n\n    refreshVisibleAnnotations() {\n        const visibleAnnotations = new Proxy(\n            {},\n            {\n                get(target, name) {\n                    return name in target ? target[name] : [];\n                },\n                set(target, name, newValue) {\n                    target[name] = newValue;\n                    return true;\n                },\n            }\n        );\n\n        this.lines.forEach((line) => {\n            line[\"visible_annotations\"] = [];\n            const lineWithoutTaxGrouping = removeTaxGroupingFromLineId(line.id);\n            if (line.visible && this.annotations[lineWithoutTaxGrouping]) {\n                for (const index in this.annotations[lineWithoutTaxGrouping]) {\n                    const annotation = this.annotations[lineWithoutTaxGrouping][index];\n                    visibleAnnotations[lineWithoutTaxGrouping] = [\n                        ...visibleAnnotations[lineWithoutTaxGrouping],\n                        { ...annotation },\n                    ];\n                    line[\"visible_annotations\"].push({\n                        ...annotation,\n                    });\n                }\n            }\n\n            if (\n                line.visible_annotations &&\n                (!this.annotations[lineWithoutTaxGrouping] || !line.visible)\n            ) {\n                delete line.visible_annotations;\n            }\n        });\n\n        this.visibleAnnotations = visibleAnnotations;\n    }\n\n    /**\n        Defines which lines should be visible in the provided list of lines (depending on what is folded).\n    **/\n    setLineVisibility(linesToAssign) {\n        let needHidingChildren = new Set();\n\n        linesToAssign.forEach((line) => {\n            line.visible = !needHidingChildren.has(line.parent_id);\n\n            if (!line.visible || (line.unfoldable &! line.unfolded))\n                needHidingChildren.add(line.id);\n        });\n\n        // If the hide 0 lines is activated we will go through the lines to set the visibility.\n        if (this.options.hide_0_lines) {\n            this.hideZeroLines(linesToAssign);\n        }\n    }\n\n    /**\n     * Defines whether the line should be visible depending on its value and the ones of its children.\n     * For parent lines, it's visible if there is at least one child with a value different from zero\n     * or if a child is visible, indicating it's a parent line.\n     * For leaf nodes, it's visible if the value is different from zero.\n     *\n     * By traversing the 'lines' array in reverse, we can set the visibility of the lines easily by keeping\n     * a dict of visible lines for each parent.\n     *\n     * @param {Object} lines - The lines for which we want to determine visibility.\n     */\n    hideZeroLines(lines) {\n        const hasVisibleChildren = new Set();\n        const reversed_lines = [...lines].reverse()\n\n        const number_figure_types = ['integer', 'float', 'monetary', 'percentage'];\n        reversed_lines.forEach((line) => {\n            const isZero = line.columns.every(column => !number_figure_types.includes(column.figure_type) || column.is_zero);\n\n            // If the line has no visible children and all the columns are equals to zero then the line needs to be hidden\n            if (!hasVisibleChildren.has(line.id) && isZero) {\n                line.visible = false;\n            }\n\n            // If the line has a parent_id and is not hidden then we fill the set 'hasVisibleChildren'. Each parent\n            // will have an array of his visible children\n            if (line.parent_id && line.visible) {\n                // This line allows the initialization of that list.\n                hasVisibleChildren.add(line.parent_id);\n            }\n        })\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Server calls\n    //------------------------------------------------------------------------------------------------------------------\n    buttonAction(ev, button) {\n        // Might be overidden to add specific functionality to button\n        // For instance adding context to a call ...\n        this.reportAction(ev, button.error_action || button.action, button.action_param, true);\n    }\n\n    async reportAction(ev, action, actionParam = null, callOnSectionsSource = false, actionContext=null) {\n        // 'ev' might be 'undefined' if event is not triggered from a button/anchor\n        ev?.preventDefault();\n        ev?.stopPropagation();\n\n        let actionOptions = this.options;\n        if (callOnSectionsSource) {\n            // When calling the sections source, we want to keep track of all unfolded lines of all sections\n            const allUnfoldedLines =  this.options.sections.length ? [] : [...this.options['unfolded_lines']]\n\n            for (const sectionData of this.options['sections']) {\n                const cacheKey = this.getCacheKey(this.options['sections_source_id'], sectionData['id']);\n                const sectionOptions = await this.reportOptionsMap[cacheKey];\n                if (sectionOptions)\n                    allUnfoldedLines.push(...sectionOptions['unfolded_lines']);\n            }\n\n            actionOptions = {...this.options, unfolded_lines: allUnfoldedLines};\n        }\n\n        const dispatchReportAction = await this.orm.call(\n            \"account.report\",\n            \"dispatch_report_action\",\n            [\n                this.options['report_id'],\n                actionOptions,\n                action,\n                actionParam,\n                callOnSectionsSource,\n            ],\n            {\n                context: Object.assign({}, this.context, actionContext)\n            }\n        );\n        if (dispatchReportAction?.help) {\n            dispatchReportAction.help = owl.markup(dispatchReportAction.help)\n        }\n\n        return dispatchReportAction ? this.actionService.doAction(dispatchReportAction) : null;\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Budget\n    // -----------------------------------------------------------------------------------------------------------------\n\n    async openBudget(budget) {\n        this.actionService.doAction({\n            type: \"ir.actions.act_window\",\n            res_model: \"account.report.budget\",\n            res_id: budget.id,\n            views: [[false, \"form\"]],\n        });\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, useState } from \"@odoo/owl\";\n\nimport { AccountReportEllipsisPopover } from \"@account_reports/components/account_report/ellipsis/popover/ellipsis_popover\";\n\nexport class AccountReportEllipsis extends Component {\n    static template = \"account_reports.AccountReportEllipsis\";\n    static props = {\n        name: { type: String, optional: true },\n        no_format: { optional: true },\n        type: { type: String, optional: true },\n        maxCharacters: Number,\n    };\n\n    setup() {\n        this.popover = useService(\"popover\");\n        this.notification = useService(\"notification\");\n        this.controller = useState(this.env.controller);\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Ellipsis\n    //------------------------------------------------------------------------------------------------------------------\n    get triggersEllipsis() {\n        if (this.props.name)\n            return this.props.name.length > this.props.maxCharacters;\n\n        return false;\n    }\n\n    copyEllipsisText() {\n        navigator.clipboard.writeText(this.props.name);\n        this.notification.add(_t(\"Text copied\"), { type: 'success' });\n        this.popoverCloseFn();\n        this.popoverCloseFn = null;\n    }\n\n    showEllipsisPopover(ev) {\n        ev.preventDefault();\n        ev.stopPropagation();\n\n        if (this.popoverCloseFn) {\n            this.popoverCloseFn();\n            this.popoverCloseFn = null;\n        }\n\n        this.popoverCloseFn = this.popover.add(\n            ev.currentTarget,\n            AccountReportEllipsisPopover,\n            {\n                name: this.props.name,\n                copyEllipsisText: this.copyEllipsisText.bind(this),\n            },\n            {\n                closeOnClickAway: true,\n                position: localization.direction === \"rtl\" ? \"left\" : \"right\",\n            },\n        );\n    }\n}\n", "/** @odoo-module */\n\nimport { Component } from \"@odoo/owl\";\n\nexport class AccountReportEllipsisPopover extends Component {\n    static template = \"account_reports.AccountReportEllipsisPopover\";\n    static props = {\n        close: Function,\n        name: String,\n        copyEllipsisText: Function,\n    };\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Component, useState } from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { WarningDialog } from \"@web/core/errors/error_dialogs\";\n\nimport { DateTimeInput } from '@web/core/datetime/datetime_input';\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { MultiRecordSelector } from \"@web/core/record_selectors/multi_record_selector\";\nimport { formatDate} from \"@web/core/l10n/dates\";\nconst { DateTime } = luxon;\n\nexport class AccountReportFilters extends Component {\n    static template = \"account_reports.AccountReportFilters\";\n    static props = {};\n    static components = {\n        DateTimeInput,\n        Dropdown,\n        DropdownItem,\n        MultiRecordSelector,\n    };\n\n    setup() {\n        this.dialog = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n        this.notification = useService(\"notification\");\n        this.companyService = useService(\"company\");\n        this.controller = useState(this.env.controller);\n        if (this.env.controller.options.date) {\n            this.dateFilter = useState(this.initDateFilters());\n        }\n        this.budgetName = useState({\n            value: \"\",\n            invalid: false,\n        });\n        this.timeout = null;\n    }\n\n    focusInnerInput(index, items) {\n        const selectedItem = items[index];\n        selectedItem.el.querySelector(\":scope input\")?.focus();\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Getters\n    //------------------------------------------------------------------------------------------------------------------\n    get selectedFiscalPositionName() {\n        switch (this.controller.options.fiscal_position) {\n            case \"domestic\":\n                return _t(\"Domestic\");\n            case \"all\":\n                return _t(\"All\");\n            default:\n                for (const fiscalPosition of this.controller.options.available_vat_fiscal_positions) {\n                    if (fiscalPosition.id === this.controller.options.fiscal_position) {\n                        return fiscalPosition.name;\n                    }\n                }\n        }\n        return _t(\"None\");\n    }\n\n    get selectedHorizontalGroupName() {\n        for (const horizontalGroup of this.controller.options.available_horizontal_groups) {\n            if (horizontalGroup.id === this.controller.options.selected_horizontal_group_id) {\n                return horizontalGroup.name;\n            }\n        }\n        return _t(\"None\");\n    }\n\n    get isHorizontalGroupSelected() {\n        return this.controller.options.available_horizontal_groups.some((group) => {\n            return group.id === this.controller.options.selected_horizontal_group_id;\n        });\n    }\n\n    get selectedTaxUnitName() {\n        for (const taxUnit of this.controller.options.available_tax_units) {\n            if (taxUnit.id === this.controller.options.tax_unit) {\n                return taxUnit.name;\n            }\n        }\n        return _t(\"Company Only\");\n    }\n\n    get selectedVariantName() {\n        for (const variant of this.controller.options.available_variants) {\n            if (variant.id === this.controller.options.selected_variant_id) {\n                return variant.name;\n            }\n        }\n        return _t(\"None\");\n    }\n\n    get selectedSectionName() {\n        for (const section of this.controller.options.sections)\n            if (section.id === this.controller.options.selected_section_id)\n                return section.name;\n    }\n\n    get selectedAccountType() {\n        let selectedAccountType = this.controller.options.account_type.filter(\n            (accountType) => accountType.selected,\n        );\n        if (\n            !selectedAccountType.length ||\n            selectedAccountType.length === this.controller.options.account_type.length\n        ) {\n            return _t(\"All\");\n        }\n\n        const accountTypeMappings = [\n            { list: [\"trade_receivable\", \"non_trade_receivable\"], name: _t(\"All Receivable\") },\n            { list: [\"trade_payable\", \"non_trade_payable\"], name: _t(\"All Payable\") },\n            { list: [\"trade_receivable\", \"trade_payable\"], name: _t(\"Trade Partners\") },\n            { list: [\"non_trade_receivable\", \"non_trade_payable\"], name: _t(\"Non Trade Partners\") },\n        ];\n\n        const listToDisplay = [];\n        for (const mapping of accountTypeMappings) {\n            if (\n                mapping.list.every((accountType) =>\n                    selectedAccountType.map((accountType) => accountType.id).includes(accountType),\n                )\n            ) {\n                listToDisplay.push(mapping.name);\n                // Delete already checked id\n                selectedAccountType = selectedAccountType.filter(\n                    (accountType) => !mapping.list.includes(accountType.id),\n                );\n            }\n        }\n\n        return listToDisplay\n            .concat(selectedAccountType.map((accountType) => accountType.name))\n            .join(\", \");\n    }\n\n    get selectedAmlIrFilters() {\n        const selectedFilters = this.controller.options.aml_ir_filters.filter(\n            (irFilter) => irFilter.selected,\n        );\n\n        if (selectedFilters.length === 1) {\n            return selectedFilters[0].name;\n        } else if (selectedFilters.length > 1) {\n            return _t(\"%s selected\", selectedFilters.length);\n        } else {\n            return _t(\"None\");\n        }\n    }\n\n    get availablePeriodOrder() {\n        return { descending: _t(\"Descending\"), ascending: _t(\"Ascending\") };\n    }\n\n    get periodOrder() {\n        return this.controller.options.comparison.period_order === \"descending\"\n            ? _t(\"Descending\")\n            : _t(\"Ascending\");\n    }\n\n    get selectedExtraOptions() {\n        const selectedExtraOptions = [];\n\n        if (this.controller.groups.account_readonly && this.controller.filters.show_draft) {\n            selectedExtraOptions.push(\n                this.controller.options.all_entries\n                    ? _t(\"With Draft Entries\")\n                    : _t(\"Posted Entries\"),\n            );\n        }\n        if (this.controller.filters.show_unreconciled && this.controller.options.unreconciled) {\n            selectedExtraOptions.push(_t(\"Unreconciled Entries\"));\n        }\n        if (this.controller.options.include_analytic_without_aml) {\n            selectedExtraOptions.push(_t(\"Including Analytic Simulations\"));\n        }\n        return selectedExtraOptions.join(\", \");\n    }\n\n    get dropdownProps() {\n        return {\n            shouldFocusChildInput: false,\n            hotkeys: {\n                arrowright: (index, items) => this.focusInnerInput(index, items),\n            },\n        };\n    }\n\n    get periodLabel() {\n        return this.controller.options.comparison.number_period > 1 ? _t(\"Periods\") : _t(\"Period\");\n    }\n    //------------------------------------------------------------------------------------------------------------------\n    // Helpers\n    //------------------------------------------------------------------------------------------------------------------\n    get hasAnalyticGroupbyFilter() {\n        return Boolean(this.controller.groups.analytic_accounting) && (Boolean(this.controller.filters.show_analytic_groupby) || Boolean(this.controller.filters.show_analytic_plan_groupby));\n    }\n\n    get hasCodesFilter() {\n        return Boolean(this.controller.options.sales_report_taxes?.operation_category?.goods);\n    }\n\n    get hasExtraOptionsFilter() {\n        return (\n            \"report_cash_basis\" in this.controller.options ||\n            this.controller.filters.show_draft ||\n            this.controller.filters.show_all ||\n            this.controller.filters.show_unreconciled ||\n            this.hasUIFilter\n        );\n    }\n\n    get hasUIFilter() {\n        return (\n            this.controller.filters.show_hide_0_lines !== \"never\" ||\n            \"horizontal_split\" in this.controller.options\n        );\n    }\n\n    get hasFiscalPositionFilter() {\n        const isMultiCompany = this.controller.options.companies.length > 1;\n        const minimumFiscalPosition = this.controller.options.allow_domestic ? 0 : 1;\n        const hasFiscalPositions =\n            this.controller.options.available_vat_fiscal_positions.length > minimumFiscalPosition;\n        return hasFiscalPositions && isMultiCompany;\n    }\n\n    get isBudgetSelected() {\n        return this.controller.options.budgets?.some((budget) => {\n            return budget.selected;\n        });\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Dates\n    //------------------------------------------------------------------------------------------------------------------\n    // Getters\n    dateFrom(optionKey) {\n        return DateTime.fromISO(this.controller.options[optionKey].date_from);\n    }\n\n    dateTo(optionKey) {\n        return DateTime.fromISO(this.controller.options[optionKey].date_to);\n    }\n\n    // Setters\n    setDate(optionKey, type, date) {\n        if (date) {\n            this.controller.options[optionKey][`date_${type}`] = date;\n            this.applyFilters(optionKey);\n        }\n        else {\n            this.dialog.add(WarningDialog, {\n                title: _t(\"Odoo Warning\"),\n                message: _t(\"Date cannot be empty\"),\n            });\n        }\n    }\n\n    setDateFrom(optionKey, dateFrom) {\n        this.setDate(optionKey, 'from', dateFrom);\n    }\n\n    setDateTo(optionKey, dateTo) {\n        this.setDate(optionKey, 'to', dateTo);\n    }\n\n    dateFilters(mode) {\n        switch (mode) {\n            case \"single\":\n                return [\n                    {\"name\": _t(\"End of Month\"), \"period\": \"month\"},\n                    {\"name\": _t(\"End of Quarter\"), \"period\": \"quarter\"},\n                    {\"name\": _t(\"End of Year\"), \"period\": \"year\"},\n                ];\n            case \"range\":\n                return [\n                    {\"name\": _t(\"Month\"), \"period\": \"month\"},\n                    {\"name\": _t(\"Quarter\"), \"period\": \"quarter\"},\n                    {\"name\": _t(\"Year\"), \"period\": \"year\"},\n                ];\n            default:\n                throw new Error(`Invalid mode in dateFilters(): ${ mode }`);\n        }\n    }\n\n    initDateFilters() {\n        const filters = {\n            \"month\": 0,\n            \"quarter\": 0,\n            \"year\": 0,\n            \"tax_period\": 0\n        };\n\n        const specifier = this.controller.options.date.filter.split('_')[0];\n        const periodType = this.controller.options.date.period_type;\n        // In case the period is fiscalyear it will be computed exactly like a year period.\n        const period = periodType === \"fiscalyear\" ? \"year\" : periodType;\n        // Set the filter value based on the specifier\n        filters[period] = this.controller.options.date.period || (specifier === 'previous' ? -1 : specifier === 'next' ? 1 : 0);\n\n        return filters;\n    }\n\n    getDateFilter(periodType) {\n        if (this.dateFilter[periodType] > 0) {\n            return `next_${periodType}`;\n        } else if (this.dateFilter[periodType] === 0) {\n            return `this_${periodType}`;\n        } else {\n            return `previous_${periodType}`;\n        }\n    }\n\n    selectDateFilter(periodType, reload = false) {\n        this.filterClicked({ optionKey: \"date.filter\", optionValue: this.getDateFilter(periodType)});\n        this.filterClicked({ optionKey: \"date.period\", optionValue: this.dateFilter[periodType], reload: reload});\n    }\n\n    selectPreviousPeriod(periodType) {\n        this._changePeriod(periodType, -1);\n    }\n\n    selectNextPeriod(periodType) {\n        this._changePeriod(periodType, 1);\n    }\n\n    _changePeriod(periodType, increment) {\n        this.dateFilter[periodType] = this.dateFilter[periodType] + increment;\n\n        this.controller.updateOption(\"date.filter\", this.getDateFilter(periodType));\n        this.controller.updateOption(\"date.period\", this.dateFilter[periodType]);\n\n        this.applyFilters(\"date.period\");\n    }\n\n    isPeriodSelected(periodType) {\n        return this.controller.options.date.filter.includes(periodType)\n    }\n\n    displayPeriod(periodType) {\n        const dateTo = DateTime.now();\n\n        switch (periodType) {\n            case \"month\":\n                return this._displayMonth(dateTo);\n            case \"quarter\":\n                return this._displayQuarter(dateTo);\n            case \"year\":\n                return this._displayYear(dateTo);\n            case \"tax_period\":\n                return this._displayTaxPeriod(dateTo);\n            default:\n                throw new Error(`Invalid period type in displayPeriod(): ${ periodType }`);\n        }\n    }\n\n    _displayMonth(dateTo) {\n        return dateTo.plus({ months: this.dateFilter.month }).toFormat(\"MMMM yyyy\");\n    }\n\n    _displayQuarter(dateTo) {\n        const quarterMonths = {\n            1: { 'start': 1, 'end': 3 },\n            2: { 'start': 4, 'end': 6 },\n            3: { 'start': 7, 'end': 9 },\n            4: { 'start': 10, 'end': 12 },\n        }\n\n        dateTo = dateTo.plus({ months: this.dateFilter.quarter * 3 });\n\n        const quarterDateFrom = DateTime.utc(dateTo.year, quarterMonths[dateTo.quarter]['start'], 1)\n        const quarterDateTo = DateTime.utc(dateTo.year, quarterMonths[dateTo.quarter]['end'], 1)\n\n        return `${ formatDate(quarterDateFrom, {format: \"MMM\"}) } - ${ formatDate(quarterDateTo, {format: \"MMM yyyy\"}) }`;\n    }\n\n    _displayYear(dateTo) {\n        return dateTo.plus({ years: this.dateFilter.year }).toFormat(\"yyyy\");\n    }\n\n    _displayTaxPeriod(dateTo) {\n        const periodicitySettings = this.controller.options.tax_periodicity;\n        const targetDateInPeriod = dateTo.plus({months: periodicitySettings.months_per_period * this.dateFilter['tax_period']})\n        const [start, end] = this._computeTaxPeriodDates(periodicitySettings, targetDateInPeriod);\n\n        if (periodicitySettings.start_month == 1 && periodicitySettings.start_day == 1) {\n            switch (periodicitySettings.months_per_period) {\n                case 1: return end.toFormat(\"MMMM yyyy\");\n                case 3: return `Q${end.quarter} ${dateTo.year}`;\n                case 12: return end.toFormat(\"yyyy\");\n            }\n        }\n\n        return formatDate(start) + ' - ' + formatDate(end);\n    }\n\n    _computeTaxPeriodDates(periodicitySettings, dateInsideTargettesPeriod) {\n        /**\n         * This function need to stay consitent with the one inside res_company from module account_reports.\n         * function_name = _get_tax_closing_period_boundaries\n         */\n        const startMonth = periodicitySettings.start_month;\n        const startDay = periodicitySettings.start_day\n        const monthsPerPeriod = periodicitySettings.months_per_period;\n        const aligned_date = dateInsideTargettesPeriod.minus({days: startDay - 1}) \n        let year = aligned_date.year;\n        const monthOffset = aligned_date.month - startMonth;\n\n        let periodNumber = Math.floor(monthOffset / monthsPerPeriod) + 1;\n\n        if (dateInsideTargettesPeriod < DateTime.now().set({year: year, month: startMonth, day: startDay})) {\n            year -= 1;\n            periodNumber = Math.floor((12 + monthOffset) / monthsPerPeriod) + 1;\n        }\n\n        let deltaMonth = periodNumber * monthsPerPeriod;\n\n        const endDate = DateTime.utc(year, startMonth, 1).plus({ months: deltaMonth, days: startDay-2})\n        const startDate = DateTime.utc(year, startMonth, 1).plus({ months: deltaMonth-monthsPerPeriod }).set({ day: startDay})\n        return [startDate, endDate];\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Number of periods\n    //------------------------------------------------------------------------------------------------------------------\n    setNumberPeriods(ev) {\n        const numberPeriods = ev.target.value;\n\n        if (numberPeriods >= 1)\n            this.controller.options.comparison.number_period = parseInt(numberPeriods);\n        else\n            this.dialog.add(WarningDialog, {\n                title: _t(\"Odoo Warning\"),\n                message: _t(\"Number of periods cannot be smaller than 1\"),\n            });\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Records\n    //------------------------------------------------------------------------------------------------------------------\n    getMultiRecordSelectorProps(resModel, optionKey) {\n        return {\n            resModel,\n            resIds: this.controller.options[optionKey],\n            update: (resIds) => {\n                this.filterClicked({ optionKey: optionKey, optionValue: resIds, reload: true});\n            },\n        };\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Rounding unit\n    //------------------------------------------------------------------------------------------------------------------\n    roundingUnitName(roundingUnit) {\n        return _t(\"In %s\", this.controller.options[\"rounding_unit_names\"][roundingUnit][0]);\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Generic filters\n    //------------------------------------------------------------------------------------------------------------------\n    async filterClicked({ optionKey, optionValue = undefined, reload = false}) {\n        if (optionValue !== undefined) {\n            await this.controller.updateOption(optionKey, optionValue);\n        } else {\n            await this.controller.toggleOption(optionKey);\n        }\n\n        if (reload) {\n            await this.applyFilters(optionKey);\n        }\n    }\n\n    async applyFilters(optionKey = null, delay = 500) {\n        // We only call the reload after the delay is finished, to avoid doing 5 calls if you want to click on 5 journals\n        if (this.timeout) {\n            clearTimeout(this.timeout);\n        }\n\n        this.controller.incrementCallNumber();\n\n        this.timeout = setTimeout(async () => {\n            await this.controller.reload(optionKey, this.controller.options);\n        }, delay);\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Custom filters\n    //------------------------------------------------------------------------------------------------------------------\n    selectJournal(journal) {\n        if (journal.model === \"account.journal.group\") {\n            const wasSelected = journal.selected;\n            this.ToggleSelectedJournal(journal);\n            this.controller.options.__journal_group_action = {\n                action: wasSelected ? \"remove\" : \"add\",\n                id: parseInt(journal.id),\n            };\n            // Toggle the selected status after the action is set\n            journal.selected = !wasSelected;\n        } else {\n            journal.selected = !journal.selected;\n        }\n        this.applyFilters(\"journals\");\n    }\n\n    ToggleSelectedJournal(selectedJournal) {\n        if (selectedJournal.selected) {\n            this.controller.options.journals.forEach((journal) => {\n                journal.selected = false;\n            });\n        } else {\n            this.controller.options.journals.forEach((journal) => {\n                journal.selected = selectedJournal.journals.includes(journal.id) && journal.model === \"account.journal\";\n            });\n        }\n    }\n\n    unfoldCompanyJournals(selectedCompany) {\n        let inSelectedCompanySection = false;\n        for (const journal of this.controller.options.journals) {\n            if (journal.id === \"divider\" && journal.model === \"res.company\") {\n                if (journal.name === selectedCompany.name) {\n                    journal.unfolded = !journal.unfolded;\n                    inSelectedCompanySection = true;\n                } else if (inSelectedCompanySection) {\n                    break;  // Reached another company divider, exit the loop\n                }\n            }\n            if (inSelectedCompanySection && journal.model === \"account.journal\") {\n                journal.visible = !journal.visible;\n            }\n        }\n    }\n\n    async filterVariant(reportId) {\n        this.controller.saveSessionOptions({\n            ...this.controller.options,\n            selected_variant_id: reportId,\n            sections_source_id: reportId,\n        });\n        const cacheKey = this.controller.getCacheKey(reportId, reportId);\n        // if the variant hasn't been loaded yet, set up the call number\n        if (!(cacheKey in this.controller.loadingCallNumberByCacheKey)) {\n            this.controller.incrementCallNumber(cacheKey);\n        }\n        await this.controller.displayReport(reportId);\n    }\n\n    async filterTaxUnit(taxUnit) {\n        await this.filterClicked({ optionKey: \"tax_unit\", optionValue: taxUnit.id});\n        this.controller.saveSessionOptions(this.controller.options);\n\n        // force the company to those impacted by the tax units, the reload will be force by this function\n        this.companyService.setCompanies(taxUnit.company_ids);\n    }\n\n    async toggleHideZeroLines() {\n        // Avoid calling the database when this filter is toggled; as the exact same lines would be returned; just reassign visibility.\n        await this.controller.toggleOption(\"hide_0_lines\", false);\n\n        this.controller.saveSessionOptions(this.controller.options);\n        this.controller.setLineVisibility(this.controller.lines);\n    }\n\n    async toggleHorizontalSplit() {\n        await this.controller.toggleOption(\"horizontal_split\", false);\n        this.controller.saveSessionOptions(this.controller.options);\n    }\n\n    async filterRoundingUnit(rounding) {\n        await this.controller.updateOption('rounding_unit', rounding, false);\n\n        this.controller.saveSessionOptions(this.controller.options);\n\n        this.controller.lines = await this.controller.orm.call(\n            \"account.report\",\n            \"format_column_values\",\n            [\n                this.controller.options,\n                this.controller.lines,\n            ],\n        );\n    }\n\n    async selectHorizontalGroup(horizontalGroupId) {\n        if (horizontalGroupId === this.controller.options.selected_horizontal_group_id) {\n            return;\n        }\n\n        if (this.isBudgetSelected) {\n            this.notification.add(\n                _t(\"It's not possible to select a budget with the horizontal group feature.\"),\n                {\n                    type: \"warning\",\n                }\n            );\n            return;\n        }\n        await this.filterClicked({ optionKey: \"selected_horizontal_group_id\", optionValue: horizontalGroupId, reload: true});\n    }\n\n    selectBudget(budget) {\n        if (this.isHorizontalGroupSelected) {\n            this.notification.add(\n                _t(\"It's not possible to select a horizontal group with the budget feature.\"),\n                {\n                    type: \"warning\",\n                }\n            );\n            return;\n        }\n        budget.selected = !budget.selected;\n        this.applyFilters( 'budgets')\n    }\n\n    async createBudget() {\n        const budgetName = this.budgetName.value.trim();\n        if (!budgetName.length) {\n            this.budgetName.invalid = true;\n            this.notification.add(_t(\"Please enter a valid budget name.\"), {\n                type: \"danger\",\n            });\n            return;\n        }\n        const createdId = await this.orm.call(\"account.report.budget\", \"create\", [\n            { name: budgetName },\n        ]);\n        this.budgetName.value = \"\";\n        this.budgetName.invalid = false;\n        const options = this.controller.options;\n        this.controller.reload(\"budgets\", {\n            ...options,\n            budgets: [\n                ...options.budgets,\n                // Selected by default if we don't have any horizontal group selected\n                { id: createdId, selected: !this.isHorizontalGroupSelected },\n            ],\n        });\n    }\n}\n", "/** @odoo-module */\n\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { useService } from \"@web/core/utils/hooks\"\n\nimport { Component, useState } from \"@odoo/owl\";\n\nexport class AccountReportHeader extends Component {\n    static template = \"account_reports.AccountReportHeader\";\n    static props = {};\n    static components = {\n        Dropdown,\n        DropdownItem,\n    }\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.controller = useState(this.env.controller);\n    }\n    // -----------------------------------------------------------------------------------------------------------------\n    // Headers\n    // -----------------------------------------------------------------------------------------------------------------\n    get columnHeaders() {\n        let columnHeaders = [];\n\n        this.controller.options.column_headers.forEach((columnHeader, columnHeaderIndex) => {\n            let columnHeadersRow = [];\n\n            for (let i = 0; i < this.controller.columnHeadersRenderData.level_repetitions[columnHeaderIndex]; i++) {\n                columnHeadersRow = [ ...columnHeadersRow, ...columnHeader];\n            }\n\n            columnHeaders.push(columnHeadersRow);\n        });\n\n        return columnHeaders;\n    }\n\n    columnHeadersColspan(column_index, header, compactOffset = 0) {\n        let colspan = header.colspan || this.controller.columnHeadersRenderData.level_colspan[column_index]\n        // In case of we need the total column for horizontal we need to increase the colspan of the first row\n        if(this.controller.options.show_horizontal_group_total && column_index === 0) {\n           colspan += 1;\n        }\n        return colspan;\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Subheaders\n    //------------------------------------------------------------------------------------------------------------------\n    get subheaders() {\n        const columns = JSON.parse(JSON.stringify(this.controller.options.columns));\n        const columnsPerGroupKey = {};\n\n        columns.forEach((column) => {\n            columnsPerGroupKey[`${column.column_group_key}_${column.expression_label}`] = column;\n        });\n\n        return this.controller.lines[0].columns.map((column) => {\n            if (columnsPerGroupKey[`${column.column_group_key}_${column.expression_label}`]) {\n                return columnsPerGroupKey[`${column.column_group_key}_${column.expression_label}`];\n            } else {\n                return {\n                    expression_label: \"\",\n                    sortable: false,\n                    name: \"\",\n                    colspan: 1,\n                };\n            }\n        });\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Custom subheaders\n    // -----------------------------------------------------------------------------------------------------------------\n    get customSubheaders() {\n        let customSubheaders = [];\n\n        this.controller.columnHeadersRenderData.custom_subheaders.forEach(customSubheader => {\n            customSubheaders.push(customSubheader);\n        });\n\n        return customSubheaders;\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Sortable\n    // -----------------------------------------------------------------------------------------------------------------\n    sortableClasses(columIndex) {\n        switch (this.controller.linesCurrentOrderByColumn(columIndex)) {\n            case \"ASC\":\n                return \"fa fa-long-arrow-up\";\n            case \"DESC\":\n                return \"fa fa-long-arrow-down\";\n            default:\n                return \"fa fa-arrows-v\";\n        }\n    }\n\n    async sortLinesByColumn(columnIndex, column) {\n        if (column.sortable) {\n            switch (this.controller.linesCurrentOrderByColumn(columnIndex)) {\n                case \"default\":\n                    await this.controller.sortLinesByColumnAsc(columnIndex);\n                    break;\n                case \"ASC\":\n                    await this.controller.sortLinesByColumnDesc(columnIndex);\n                    break;\n                case \"DESC\":\n                    this.controller.sortLinesByDefault();\n                    break;\n                default:\n                    throw new Error(`Invalid value: ${ this.controller.linesCurrentOrderByColumn(columnIndex) }`);\n            }\n        }\n    }\n}\n", "/** @odoo-module */\n\nimport { localization } from \"@web/core/l10n/localization\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, useState } from \"@odoo/owl\";\n\nimport { AccountReportDebugPopover } from \"@account_reports/components/account_report/line/popover/debug_popover\";\nimport { AccountReportLineCellEditable } from \"@account_reports/components/account_report/line_cell_editable/line_cell_editable\";\n\nexport class AccountReportLine extends Component {\n    static template = \"account_reports.AccountReportLine\";\n    static props = {\n        lineIndex: Number,\n        line: Object,\n    };\n    static components = {\n        AccountReportLineCellEditable,\n    };\n\n    setup() {\n        this.popover = useService(\"popover\");\n        this.controller = useState(this.env.controller);\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Line\n    // -----------------------------------------------------------------------------------------------------------------\n    get lineClasses() {\n        let classes = ('level' in this.props.line) ? `line_level_${this.props.line.level}` : 'line_level_default';\n\n        if (!this.props.line.visible || this.isHiddenBySearchFilter())\n            classes += \" d-none\";\n\n        if (this.props.line.unfolded && this.hasVisibleChild())\n            classes += \" unfolded\";\n\n        if (this.controller.isTotalLine(this.props.lineIndex))\n            classes += \" total\";\n\n        if (this.props.line.class)\n            classes += ` ${this.props.line.class}`;\n\n        return classes;\n    }\n\n    hasVisibleChild() {\n        let nextLineIndex = this.props.lineIndex + 1;\n\n        while (this.controller.isNextLineChild(nextLineIndex, this.props.line['id'])) {\n            if (this.controller.lines[nextLineIndex].visible && !this.isHiddenBySearchFilter(this.controller.lines[nextLineIndex].id))\n                return true;\n\n            nextLineIndex += 1;\n        }\n        return false;\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Growth comparison\n    // -----------------------------------------------------------------------------------------------------------------\n    get growthComparisonClasses() {\n        let classes = \"text-end\";\n\n        switch(this.props.line.column_percent_comparison_data.mode) {\n            case \"green\":\n                classes += \" text-success\";\n                break;\n            case \"muted\":\n                classes += \" muted\";\n                break;\n            case \"red\":\n                classes += \" text-danger\";\n                break;\n        }\n\n        return classes;\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Total Horizontal Group\n    // -----------------------------------------------------------------------------------------------------------------\n    get HorizontalGroupTotalClasses() {\n        let classes = \"text-end\";\n        switch(Math.sign(this.props.line.horizontal_group_total_data?.no_format)) {\n            case 1:\n                break;\n            case 0:\n                classes += \" muted\";\n                break;\n            case -1:\n                classes += \" text-danger\";\n                break;\n        }\n\n        return classes;\n    }\n\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Search\n    //------------------------------------------------------------------------------------------------------------------\n    isHiddenBySearchFilter(lineId = null) {\n        // If no lineId is provided, this will execute on the current line object\n        // Otherwise, it will execute on the given lineId\n        lineId ||= this.props.line.id;\n\n        if (!(\"lines_searched\" in this.controller))\n            return false;\n\n        for (let searchLineId of this.controller.lines_searched)\n            if (this.controller.isLineRelatedTo(searchLineId, lineId) || lineId === searchLineId)\n                return false;\n\n        return true;\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Debug popover\n    //------------------------------------------------------------------------------------------------------------------\n    showDebugPopover(ev) {\n        const close = () => {\n            this.popoverCloseFn();\n            this.popoverCloseFn = null;\n        }\n\n        if (this.popoverCloseFn)\n            close();\n\n        this.popoverCloseFn = this.popover.add(\n            ev.currentTarget,\n            AccountReportDebugPopover,\n            {\n                expressionsDetail: JSON.parse(this.props.line.debug_popup_data).expressions_detail,\n                onClose: close,\n            },\n            {\n                closeOnClickAway: true,\n                position: localization.direction === \"rtl\" ? \"left\" : \"right\",\n            },\n        );\n    }\n}\n", "/** @odoo-module */\n\nimport { Component } from \"@odoo/owl\";\n\nexport class AccountReportDebugPopover extends Component {\n    static template = \"account_reports.AccountReportDebugPopover\";\n    static props = {\n        close: Function,\n        expressionsDetail: Array,\n        onClose: Function,\n    };\n}\n", "/** @odoo-module */\n\nimport { localization } from \"@web/core/l10n/localization\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { AccountReportCarryoverPopover } from \"@account_reports/components/account_report/line_cell/popover/carryover_popover\";\nimport { AccountReportEditPopover } from \"@account_reports/components/account_report/line_cell/popover/edit_popover\";\n\nimport { Component, markup, useState } from \"@odoo/owl\";\n\nexport class AccountReportLineCell extends Component {\n    static template = \"account_reports.AccountReportLineCell\";\n    static props = {\n        line: {\n            type: Object,\n            optional: true,\n        },\n        cell: Object,\n    };\n\n    setup() {\n        this.action = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.popover = useService(\"popover\");\n        this.controller = useState(this.env.controller);\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Helpers\n    // -----------------------------------------------------------------------------------------------------------------\n    isNumeric(type) {\n        return ['float', 'integer', 'monetary', 'percentage'].includes(type);\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Attributes\n    // -----------------------------------------------------------------------------------------------------------------\n    get cellClasses() {\n        if (this.props.cell.comparison_mode) {\n            return this.comparisonClasses;\n        }\n\n        let classes = \"\";\n\n        if (this.props.cell.auditable)\n            classes += \" auditable\";\n\n        if (this.props.cell.figure_type === 'date')\n            classes += \" date\";\n\n        if (this.props.cell.figure_type === 'string')\n            classes += \" text\";\n\n        if (this.isNumeric(this.props.cell.figure_type)) {\n            classes += \" numeric text-end\";\n\n            if (this.props.cell.no_format !== undefined)\n                switch (Math.sign(this.props.cell.no_format)) {\n                    case 1:\n                        break;\n                    case 0:\n                    case -0:\n                        classes += \" muted\";\n                        break;\n                    case -1:\n                        classes += \" text-danger\";\n                        break;\n                }\n        }\n\n        if (this.props.cell.class)\n            classes += ` ${this.props.cell.class}`;\n\n        return classes;\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Audit\n    // -----------------------------------------------------------------------------------------------------------------\n    async audit() {\n        const auditAction = await this.orm.call(\n            \"account.report\",\n            \"dispatch_report_action\",\n            [\n                this.controller.options.report_id,\n                this.controller.options,\n                \"action_audit_cell\",\n                {\n                    report_line_id: this.props.cell.report_line_id,\n                    expression_label: this.props.cell.expression_label,\n                    calling_line_dict_id: this.props.line.id,\n                    column_group_key: this.props.cell.column_group_key,\n                },\n            ],\n            {\n                context: this.controller.context,\n            }\n        );\n        if (auditAction.help) {\n            auditAction.help = markup(auditAction.help);\n        }\n\n        return this.action.doAction(auditAction);\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Edit Popover\n    // -----------------------------------------------------------------------------------------------------------------\n    editPopover(ev) {\n        const close = () => {\n            this.popoverCloseFn();\n            this.popoverCloseFn = null;\n        }\n\n        if (this.popoverCloseFn)\n            close();\n\n        this.popoverCloseFn = this.popover.add(\n            ev.currentTarget,\n            AccountReportEditPopover,\n            {\n                line_id: this.props.line.id,\n                cell: this.props.cell,\n                controller: this.controller,\n                onClose: close,\n            },\n            {\n                closeOnClickAway: true,\n                position: localization.direction === \"rtl\" ? \"bottom\" : \"left\",\n            },\n        );\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Carryover popover\n    //------------------------------------------------------------------------------------------------------------------\n    carryoverPopover(ev) {\n        if (this.popoverCloseFn) {\n            this.popoverCloseFn();\n            this.popoverCloseFn = null;\n        }\n\n        this.popoverCloseFn = this.popover.add(\n            ev.currentTarget,\n            AccountReportCarryoverPopover,\n            {\n                carryoverData: JSON.parse(this.props.cell.info_popup_data),\n                options: this.controller.options,\n                context: this.controller.context,\n            },\n            {\n                closeOnClickAway: true,\n                position: localization.direction === \"rtl\" ? \"bottom\" : \"right\",\n            },\n        );\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Comparison cell\n    // -----------------------------------------------------------------------------------------------------------------\n    get comparisonClasses() {\n        let classes = \"text-end\";\n\n        switch (this.props.cell.comparison_mode) {\n            case \"green\":\n                classes += \" text-success\";\n                break;\n            case \"muted\":\n                classes += \" muted\";\n                break;\n            case \"red\":\n                classes += \" text-danger\";\n                break;\n        }\n\n        return classes;\n    }\n}\n", "/** @odoo-module */\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component } from \"@odoo/owl\";\n\nexport class AccountReportCarryoverPopover extends Component {\n    static template = \"account_reports.AccountReportCarryoverPopover\";\n    static props = {\n        close: Function,\n        carryoverData: Object,\n        options: Object,\n        context: Object,\n    };\n\n    setup() {\n        this.actionService = useService(\"action\");\n        this.orm = useService(\"orm\");\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    //\n    //------------------------------------------------------------------------------------------------------------------\n    async viewCarryoverLinesAction(expressionId, columnGroupKey) {\n        const viewCarryoverLinesAction = await this.orm.call(\n            \"account.report.expression\",\n            \"action_view_carryover_lines\",\n            [\n                expressionId,\n                this.props.options,\n                columnGroupKey,\n            ],\n            {\n                context: this.props.context,\n            }\n        );\n        this.props.close()\n        return this.actionService.doAction(viewCarryoverLinesAction);\n    }\n}\n", "/** @odoo-module */\n\nimport { useAutofocus, useService } from \"@web/core/utils/hooks\";\n\nimport { Component, useRef } from \"@odoo/owl\";\n\nexport class AccountReportEditPopover extends Component {\n    static template = \"account_reports.AccountReportEditPopover\";\n    static props = {\n        line_id: String,\n        cell: Object,\n        controller: Object,\n        onClose: Function,\n        close: Function,\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n\n        if (this.props.cell.figure_type === 'boolean') {\n            this.booleanTrue = useRef(\"booleanTrue\");\n            this.booleanFalse = useRef(\"booleanFalse\");\n        } else {\n            this.input = useRef(\"input\");\n            useAutofocus({ refName: \"input\" });\n        }\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Edit\n    // -----------------------------------------------------------------------------------------------------------------\n    async edit() {\n        let editValue;\n        const editPopupData = JSON.parse(this.props.cell.edit_popup_data);\n\n        if (this.props.cell.figure_type === 'boolean')\n            editValue = Number(this.booleanTrue.el.checked && !this.booleanFalse.el.checked);\n        else\n            editValue = this.input.el.value;\n\n        const res = await this.orm.call(\n            \"account.report\",\n            \"action_modify_manual_value\",\n            [\n                this.props.controller.options.report_id,\n                this.props.line_id,\n                this.props.controller.options,\n                editPopupData.column_group_key,\n                editValue,\n                editPopupData.target_expression_id,\n                editPopupData.rounding,\n                this.props.controller.columnGroupsTotals,\n            ],\n            {\n                context: this.props.controller.context,\n            }\n        );\n\n        this.props.controller.lines = res.lines;\n        this.props.controller.columnGroupsTotals = res.column_groups_totals;\n\n        this.props.onClose();\n    }\n}\n", "import { useRef, useState } from \"@odoo/owl\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\n\nimport { AccountReportLineCell } from \"@account_reports/components/account_report/line_cell/line_cell\";\n\nexport class AccountReportLineCellEditable extends AccountReportLineCell {\n    static template = \"account_reports.AccountReportLineCellEditable\";\n\n    setup() {\n        super.setup();\n        this.input = useRef(\"input\");\n        this.focused = useState({ value: false });\n\n        useHotkey(\n            \"Enter\",\n            (ev) => {\n                ev.target.blur();\n            },\n            { bypassEditableProtection: true }\n        );\n    }\n\n    get cellClasses() {\n        let classes = super.cellClasses;\n        if (this.hasEditPopupData) {\n            classes += \" editable-cell\";\n        }\n        return classes;\n    }\n\n    get hasEditPopupData() {\n        return Boolean(this.props.cell?.edit_popup_data);\n    }\n\n    async onChange() {\n        if (!this.input.el.value.trim()) {\n            return;\n        }\n        const editValue = this.input.el.value;\n        const cellEditData = this.hasEditPopupData\n            ? JSON.parse(this.props.cell.edit_popup_data)\n            : {};\n\n        const res = await this.orm.call(\n            \"account.report\",\n            \"action_modify_manual_value\",\n            [\n                this.controller.options.report_id,\n                this.props.line.id,\n                this.controller.options,\n                cellEditData.column_group_key,\n                editValue,\n                cellEditData.target_expression_id,\n                cellEditData.rounding,\n                this.controller.columnGroupsTotals,\n            ],\n            {\n                context: this.controller.context,\n            }\n        );\n        this.controller.lines = res.lines;\n        this.controller.columnGroupsTotals = res.column_groups_totals;\n        this.focused.value = false;\n    }\n\n    get inputValue() {\n        return this.focused.value ? this.props.cell.no_format : this.props.cell.name;\n    }\n\n    onFocus() {\n        this.focused.value = true;\n    }\n\n    onBlur() {\n        if (JSON.stringify(this.props.cell.no_format) === this.input.el.value) {\n            this.focused.value = false;\n        }\n    }\n}\n", "/** @odoo-module */\n\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { Component, useState, useRef } from \"@odoo/owl\";\n\nimport { AccountReportAnnotationsPopover } from \"@account_reports/components/account_report/line_name/popover/annotations_popover\";\n\nexport class AccountReportLineName extends Component {\n    static template = \"account_reports.AccountReportLineName\";\n    static props = {\n        lineIndex: Number,\n        line: Object,\n    };\n    static components = {\n        Dropdown,\n        DropdownItem,\n    }\n\n    setup() {\n        this.action = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.controller = useState(this.env.controller);\n        this.annotationPopOver = usePopover(AccountReportAnnotationsPopover, {\n            setActiveElement: false,\n            position: \"bottom\",\n            animation: false,\n            closeOnClickAway: (target) => !target.closest(\".annotation_popover\"),\n        });\n\n        this.lineNameCell = useRef(\"lineNameCell\");\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Caret options\n    //------------------------------------------------------------------------------------------------------------------\n    get caretOptions() {\n        return this.controller.caretOptions[this.props.line.caret_options];\n    }\n\n    get hasCaretOptions() {\n        return this.caretOptions?.length > 0;\n    }\n\n    async caretAction(caretOption) {\n        const res = await this.orm.call(\n            \"account.report\",\n            \"dispatch_report_action\",\n            [\n                this.controller.options.report_id,\n                this.controller.options,\n                caretOption.action,\n                {\n                    line_id: this.props.line.id,\n                    action_param: caretOption.action_param,\n                },\n            ],\n            {\n                context: this.controller.context,\n            }\n        );\n\n        return this.action.doAction(res);\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Classes\n    // -----------------------------------------------------------------------------------------------------------------\n    get lineNameClasses() {\n        let classes = \"text\";\n\n        if (this.props.line.unfoldable)\n            classes += \" unfoldable\";\n\n        if (this.props.line.class)\n            classes += ` ${ this.props.line.class }`;\n\n        return classes;\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Action\n    // -----------------------------------------------------------------------------------------------------------------\n    async triggerAction() {\n        const res = await this.orm.call(\n            \"account.report\",\n            \"execute_action\",\n            [\n                this.controller.options.report_id,\n                this.controller.options,\n                {\n                    id: this.props.line.id,\n                    actionId: this.props.line.action_id,\n                },\n            ],\n            {\n                context: this.controller.context,\n            }\n        );\n\n        return this.action.doAction(res);\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Load more\n    // -----------------------------------------------------------------------------------------------------------------\n    async loadMore() {\n        const newLines = await this.orm.call(\n            \"account.report\",\n            \"get_expanded_lines\",\n            [\n                this.controller.options.report_id,\n                this.controller.options,\n                this.props.line.parent_id,\n                this.props.line.groupby,\n                this.props.line.expand_function,\n                this.props.line.progress,\n                this.props.line.offset,\n                this.props.line.horizontal_split_side,\n            ],\n        );\n\n        this.controller.setLineVisibility(newLines)\n        if (this.controller.areLinesOrdered()) {\n            this.controller.updateLinesOrderIndexes(this.props.lineIndex, newLines, true)\n        }\n        await this.controller.replaceLineWith(this.props.lineIndex, newLines);\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Fold / Unfold\n    // -----------------------------------------------------------------------------------------------------------------\n    toggleFoldable() {\n        if (this.props.line.unfoldable)\n            if (this.props.line.unfolded)\n                this.controller.foldLine(this.props.lineIndex);\n            else\n                this.controller.unfoldLine(this.props.lineIndex);\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Annotation\n    // -----------------------------------------------------------------------------------------------------------------\n    get hasVisibleAnnotation() {\n        return this.props.line.visible_annotations;\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Annotation Popover\n    //------------------------------------------------------------------------------------------------------------------\n    async toggleAnnotationPopover() {\n        if (this.annotationPopOver.isOpen) {\n            this.annotationPopOver.close();\n        } else {\n            this.annotationPopOver.open(this.lineNameCell.el, {\n                controller: this.controller,\n                lineName: this,\n                lineID: this.props.line.id,\n            });\n        }\n    }\n\n    async addAnnotation(ev) {\n        this.annotationPopOver.open(this.lineNameCell.el, {\n            controller: this.controller,\n            lineName: this,\n            isAddingAnnotation: true,\n            lineID: this.props.line.id,\n        });\n    }\n}\n", "import { Component, useState, useRef, onWillDestroy } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { DateTimeInput } from \"@web/core/datetime/datetime_input\";\nimport { AnnotationPopoverLine } from \"@account_reports/components/account_report/line_name/popover_line/annotation_popover_line\";\nimport { removeTaxGroupingFromLineId } from \"@account_reports/js/util\";\n\nconst { DateTime } = luxon;\n\nexport class AccountReportAnnotationsPopover extends Component {\n    static template = \"account_reports.AccountReportAnnotationsPopover\";\n    static props = {\n        controller: Object,\n        lineName: Object,\n        lineID: String,\n        close: { type: Function, optional: true },\n        isAddingAnnotation: { type: Boolean, optional: true },\n    };\n    static components = {\n        DateTimeInput,\n        AnnotationPopoverLine,\n    };\n\n    setup() {\n        this.notificationService = useService(\"notification\");\n\n        this.newAnnotation = useState({\n            value: this.props.isAddingAnnotation ? this._getNewAnnotation() : {},\n        });\n\n        this.annotations = useState(this.props.controller.visibleAnnotations[removeTaxGroupingFromLineId(this.props.lineID)]);\n\n        this.popoverTable = useRef(\"popoverTable\");\n        this.currentPromise = null;\n\n        onWillDestroy(async () => {\n            if (this.currentPromise) {\n                await this.currentPromise;\n            }\n        });\n    }\n\n    get isAddingAnnotation() {\n        return Object.keys(this.newAnnotation.value).length !== 0;\n    }\n\n    async refreshAnnotations() {\n        this.currentPromise = null;\n        await this.props.controller.refreshAnnotations();\n        this.annotations = this.props.controller.visibleAnnotations[removeTaxGroupingFromLineId(this.props.lineID)];\n        if (this.isAddingAnnotation) {\n            this.cleanNewAnnotation();\n        }\n    }\n\n    _getNewAnnotation() {\n        const date =\n            this.props.controller.options.date.filter === \"today\"\n                ? new Date().toISOString().split(\"T\")[0]\n                : this.props.controller.options.date.date_to;\n        return {\n            date: DateTime.fromISO(date),\n            text: \"\",\n            lineID: this.props.lineID,\n        };\n    }\n\n    cleanNewAnnotation() {\n        this.newAnnotation.value = {};\n    }\n\n    addAnnotation() {\n        this.newAnnotation.value = this._getNewAnnotation();\n    }\n\n    formatAnnotation(annotation) {\n        return {\n            id: annotation.id,\n            date: annotation.date ? DateTime.fromISO(annotation.date) : null,\n            text: annotation.text,\n            lineID: annotation.line_id,\n        };\n    }\n\n    async saveNewAnnotation(newAnnotation) {\n        if (newAnnotation.text) {\n            this.currentPromise = this.env.services.orm.call(\n                \"account.report.annotation\",\n                \"create\",\n                [\n                    {\n                        report_id: this.props.controller.options.report_id,\n                        line_id: newAnnotation.lineID,\n                        text: newAnnotation.text,\n                        date: newAnnotation.date ? newAnnotation.date.toFormat(\"yyyy-LL-dd\") : null,\n                        fiscal_position_id: this.props.controller.options.fiscal_position,\n                    },\n                ],\n                {\n                    context: this.props.context,\n                }\n            );\n            // We're using a .then() here to make sure that even if the component is destroyed\n            // we'll call the function to finalize the logic.\n            this.currentPromise.then(async () => {\n                await this.refreshAnnotations();\n                if (this.popoverTable.el) {\n                    this.popoverTable.el.scrollIntoView({ behavior: \"smooth\", block: \"end\" });\n                }\n            });\n        }\n    }\n\n    async deleteAnnotation(annotationId) {\n        this.currentPromise = this.env.services.orm.call(\n            \"account.report.annotation\",\n            \"unlink\",\n            [annotationId],\n            { context: this.props.controller.context }\n        );\n        await this.currentPromise;\n        await this.refreshAnnotations();\n    }\n\n    async editAnnotation(editedAnnotation, existingAnnotation) {\n        this.currentPromise = this.env.services.orm.call(\n            \"account.report.annotation\",\n            \"write\",\n            [[existingAnnotation.id], { text: editedAnnotation.text, date: editedAnnotation.date }],\n            {\n                context: this.props.controller.context,\n            }\n        );\n        await this.currentPromise;\n        await this.refreshAnnotations();\n    }\n}\n", "import { Component, useState, useRef, useEffect } from \"@odoo/owl\";\nimport { useAutofocus, useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { DateTimeInput } from \"@web/core/datetime/datetime_input\";\n\nconst { DateTime } = luxon;\n\nexport class AnnotationPopoverLine extends Component {\n    static template = \"account_report.AnnotationPopoverLine\";\n    static props = {\n        annotation: {\n            type: Object,\n            shape: {\n                date: { type: [DateTime, { value: false }, { value: null }], optional: true },\n                text: String,\n                lineID: String,\n                id: { type: Number, optional: true },\n            },\n        },\n        onEdit: Function,\n        onDelete: Function,\n    };\n    static components = {\n        DateTimeInput,\n    };\n\n    setup() {\n        this.applyAutoresizeToAll(\".annotation_popover_autoresize_textarea\");\n        this.annotation = useState(this.props.annotation);\n        this.notificationService = useService(\"notification\");\n        if (this.annotation.text.length) {\n            this.textArea = useRef(\"annotationText\");\n        } else {\n            this.textArea = useAutofocus({ refName: \"annotationText\" });\n        }\n    }\n\n    applyAutoresizeToAll(selector) {\n        useEffect(() => {\n            const resizeTextArea = (textarea) => {\n                textarea.style.height = `${textarea.scrollHeight}px`;\n                textarea.parentElement.style.height = `${textarea.scrollHeight}px`;\n            };\n\n            const textareas = document.querySelectorAll(selector);\n            for (const textarea of textareas) {\n                Object.assign(textarea.style, {\n                    width: \"auto\",\n                    paddingTop: 0,\n                    paddingBottom: 0,\n                });\n                resizeTextArea(textarea);\n                textarea.addEventListener(\"input\", () => {\n                    resizeTextArea(textarea);\n                });\n            }\n        });\n    }\n\n    annotationEditDate(date) {\n        this.annotation.date = date;\n        this.props.onEdit(this.annotation);\n    }\n\n    annotationEditText() {\n        if (this.textArea.el.value) {\n            if (this.textArea.el.value !== this.annotation.text) {\n                this.annotation.text = this.textArea.el.value;\n                this.props.onEdit(this.annotation);\n            }\n        } else {\n            this.notificationService.add(_t(\"The annotation shouldn't have an empty value.\"));\n        }\n    }\n\n    deleteAnnotation() {\n        this.props.onDelete(this.annotation.id);\n    }\n}\n", "/** @odoo-module */\n\nimport { Component, useRef, useState, onMounted } from \"@odoo/owl\";\n\nexport class AccountReportSearchBar extends Component {\n    static template = \"account_reports.AccountReportSearchBar\";\n    static props = {\n        initialQuery: { type: String, optional: true },\n    };\n\n    setup() {\n        this.searchText = useRef(\"search_bar_input\");\n        this.controller = useState(this.env.controller);\n\n        onMounted(() => {\n            if (this.props.initialQuery) {\n                this.searchText.el.value = this.props.initialQuery;\n                this.search();\n            }\n        });\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Search\n    //------------------------------------------------------------------------------------------------------------------\n    search() {\n        const inputText = this.searchText.el.value.trim();\n        const query = inputText.toLowerCase();\n        const linesIDsMatched = [];\n\n        if (query.length) {\n            for (const line of this.controller.lines) {\n                if (!line.name) continue;\n\n                const lineName = line.name.trim().toLowerCase();\n                const match = (lineName.indexOf(query) !== -1);\n\n                if (match) {\n                    linesIDsMatched.push(line.id);\n                }\n            }\n            this.controller.lines_searched = linesIDsMatched;\n            this.controller.updateOption(\"filter_search_bar\", inputText);\n        } else {\n            delete this.controller.lines_searched;\n            this.controller.deleteOption(\"filter_search_bar\");\n        }        \n    }\n}\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { WarningDialog } from \"@web/core/errors/error_dialogs\";\nimport { AccountReport } from \"@account_reports/components/account_report/account_report\";\nimport { AccountReportFilters } from \"@account_reports/components/account_report/filters/filters\";\n\nexport class AgedPartnerBalanceFilters extends AccountReportFilters {\n    static template = \"account_reports.AgedPartnerBalanceFilters\";\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Aging Interval\n    //------------------------------------------------------------------------------------------------------------------\n    async setAgingInterval(ev) {\n        const agingInterval = parseInt(ev.target.value);\n        if (agingInterval < 1) {\n            this.dialog.add(WarningDialog, {\n                title: _t(\"Odoo Warning\"),\n                message: _t(\"Intervals cannot be smaller than 1\"),\n            });\n            return;\n        }\n\n        await this.filterClicked({ optionKey:\"aging_interval\", optionValue: agingInterval, reload: true });\n    }\n\n}\n\nAccountReport.registerCustomComponent(AgedPartnerBalanceFilters);\n", "/** @odoo-module */\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { JournalDashboardActivity, journalDashboardActivity } from \"@account/components/journal_dashboard_activity/journal_dashboard_activity\";\n\nexport class JournalDashboardActivityTaxReport extends JournalDashboardActivity {\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n    }\n\n    async openActivity(activity) {\n        if (activity.activity_category === 'tax_report') {\n            const act = await this.orm.call(\"mail.activity\", \"action_open_tax_activity\", [activity.id], {});\n            this.action.doAction(act);\n        } else {\n            super.openActivity(activity);\n        }\n    }\n}\n\nexport const journalDashboardActivityTaxReport = {\n    ...journalDashboardActivity,\n    component: JournalDashboardActivityTaxReport,\n};\n\nregistry\n    .category(\"fields\")\n    .add(\"kanban_vat_activity\", journalDashboardActivityTaxReport, { force: true });\n", "import { patch } from \"@web/core/utils/patch\";\nimport { AccountReportFilters } from \"@account_reports/components/account_report/filters/filters\";\n\npatch(AccountReportFilters.prototype, {\n    get hasExtraOptionsFilter() {\n        return super.hasExtraOptionsFilter || \"show_payment_lines\" in this.controller.options;\n    },\n});\n", "/** @odoo-module */\n\nimport { AccountReport } from \"@account_reports/components/account_report/account_report\";\nimport { AccountReportLine } from \"@account_reports/components/account_report/line/line\";\n\nexport class JournalReportLine extends AccountReportLine {\n    static template = \"account_reports.JournalReportLine\";\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Classes\n    // -----------------------------------------------------------------------------------------------------------------\n    get lineClasses() {\n        let classes = super.lineClasses;\n\n        if (this.props.line.id.includes(\"|headers~~\"))\n            classes += ' accent_header';\n\n        if (this.props.line.move_id)\n            classes += ' accent_line';\n\n        return classes;\n    }\n\n    // -----------------------------------------------------------------------------------------------------------------\n    // Actions\n    // -----------------------------------------------------------------------------------------------------------------\n    async openTaxJournalItems(ev, name, taxType) {\n        return this.controller.reportAction(ev, \"journal_report_action_open_tax_journal_items\", {\n            name: name,\n            tax_type: taxType,\n            journal_id: this.props.line.journal_id,\n            journal_type: this.props.line.journal_type,\n            date_form: this.props.line.date_from,\n            date_to: this.props.line.date_to,\n        })\n    }\n}\n\nAccountReport.registerCustomComponent(JournalReportLine);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { AccountMoveLineListRenderer, AccountMoveLineListView } from \"@account_accountant/components/move_line_list/move_line_list\";\n\n\nexport class JournalReportAccountMoveLineReconcileListRenderer extends AccountMoveLineListRenderer {\n    setup() {\n        super.setup();\n        this.props.list.groups?.forEach(group => {\n            group.list?.groups?.forEach(innerGroup => {\n                this.toggleGroup(innerGroup);\n            });\n        });\n    }\n}\n\nexport const JournalReportAccountMoveLineReconcileLineListView = {\n    ...AccountMoveLineListView,\n    Renderer: JournalReportAccountMoveLineReconcileListRenderer,\n};\n\nregistry.category(\"views\").add(\"account_move_line_journal_report_list\", JournalReportAccountMoveLineReconcileLineListView);\n", "import { AccountReport } from \"@account_reports/components/account_report/account_report\";\nimport { AccountReportFilters } from \"@account_reports/components/account_report/filters/filters\";\n\nexport class MulticurrencyRevaluationReportFilters extends AccountReportFilters {\n    static template = \"account_reports.MulticurrencyRevaluationReportFilters\";\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Custom filters\n    //------------------------------------------------------------------------------------------------------------------\n    async filterExchangeRate(ev, currencyId) {\n        this.controller.options.currency_rates[currencyId].rate = ev.currentTarget.value;\n    }\n}\n\nAccountReport.registerCustomComponent(MulticurrencyRevaluationReportFilters);\n", "\n/** @odoo-module */\n\nimport { AccountReport } from \"@account_reports/components/account_report/account_report\";\nimport { AccountReportLineCell } from \"@account_reports/components/account_report/line_cell/line_cell\";\nconst { DateTime } = luxon;\n\n\nexport class PartnerLedgerLineCell extends AccountReportLineCell {\n    static template = \"account_reports.PartnerLedgerLineCell\";\n    get cellClasses() {\n        let superCellClasses = super.cellClasses;\n        const cell = this.props.cell;\n        if (\n            cell.figure_type === 'date'\n            && cell.expression_label == 'date_maturity'\n            && cell.no_format\n            && DateTime.fromISO(cell.no_format).startOf('day') < DateTime.now().startOf('day')\n        ) {\n            superCellClasses += ' text-danger';\n        }\n        return superCellClasses;\n    }\n}\n\nAccountReport.registerCustomComponent(PartnerLedgerLineCell);\n", "/** @odoo-module */\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component } from \"@odoo/owl\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\n\nclass RedirectAction extends Component {\n    static template = \"account_reports.redirectAction\";\n    static components = { Dialog };\n\n    static props = {\n        ...standardActionServiceProps,\n        action: Object\n    }\n\n    setup() {\n        this.actionService = useService(\"action\");\n    }\n\n    openClientAction() {\n        this.actionService.doAction(this.props.action.params.depending_action);\n    }\n\n    close() {\n        this.actionService.doAction({type: 'ir.actions.act_window_close'});\n    }\n}\n\nregistry.category(\"actions\").add(\"account_reports.redirect_action\", RedirectAction);\n", "import { _t } from \"@web/core/l10n/translation\";\n\nimport { AccountReport } from \"@account_reports/components/account_report/account_report\";\nimport { AccountReportFilters } from \"@account_reports/components/account_report/filters/filters\";\n\nexport class SalesReportFilters extends AccountReportFilters {\n    static template = \"account_reports.SalesReportFilters\";\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Getters\n    //------------------------------------------------------------------------------------------------------------------\n    get selectedEcTaxName() {\n        const selected = this.controller.options.ec_tax_filter_selection.filter(\n            (ecTax) => ecTax.selected,\n        );\n\n        switch (selected.length) {\n            case this.controller.options.ec_tax_filter_selection.length:\n                return _t(\"All\");\n            case 0:\n                return _t(\"None\");\n            default:\n                return selected.map((s) => s.name.substring(0, 1)).join(\", \");\n        }\n    }\n}\n\nAccountReport.registerCustomComponent(SalesReportFilters);\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { download } from \"@web/core/network/download\";\n\nasync function executeAccountReportDownload({ env, action }) {\n    env.services.ui.block();\n\n    const url = \"/account_reports\";\n    const data = action.data;\n\n    try {\n        await download({ url, data });\n        env.services.action.doAction({type: 'ir.actions.act_window_close'});\n    } catch (e) {\n        if (e.exceptionName === 'AccountReportFileDownloadException') {\n            const reportOptions = JSON.parse(data.options);\n            const reportAction = await env.services.orm.call(\n                'account.report',\n                'open_account_report_file_download_error_wizard',\n                [reportOptions.report_id, e.data.arguments[0], e.data.arguments[1]],\n            );\n            env.services.action.doAction(reportAction);\n        } else {\n            throw e;\n        }\n    } finally {\n        env.services.ui.unblock();\n    }\n}\n\nregistry\n    .category(\"action_handlers\")\n    .add('ir_actions_account_report_download', executeAccountReportDownload);\n", "const LINE_ID_HIERARCHY_DELIMITER = \"|\";\nconst LINE_ID_GROUP_DELIMITER = \"~\";\n\nexport function buildLineId(current) {\n    const convertNull = (value) => {\n        if (value === null || value === undefined || value === false) {\n            return \"\";\n        } else if (typeof value === \"object\") {\n            // We're using the replaceAll to follow the python\n            // behavior where we're stringify a dict.\n            return JSON.stringify(value).replaceAll('\":', '\": ');\n        }\n        return value;\n    };\n\n    return current\n        .map(([markup, model, value]) => {\n            const lineValues = [convertNull(markup), convertNull(model), convertNull(value)];\n            return lineValues.join(LINE_ID_GROUP_DELIMITER);\n        })\n        .join(LINE_ID_HIERARCHY_DELIMITER);\n}\n\nexport function parseLineId(lineID, markupAsString = false) {\n    const parseMarkup = (markup) => {\n        if (!markup) {\n            return markup;\n        }\n        try {\n            const result = JSON.parse(markup);\n            return typeof result === \"object\" ? result : markup;\n        } catch {\n            return markup;\n        }\n    };\n\n    if (!lineID) {\n        return [];\n    }\n    return lineID.split(LINE_ID_HIERARCHY_DELIMITER).map((key) => {\n        const [markup, model, value] = key.split(LINE_ID_GROUP_DELIMITER);\n        return [\n            (markupAsString ? markup : parseMarkup(markup)) || null,\n            model || null,\n            model && value ? parseInt(value) : value || null,\n        ];\n    });\n}\n\nexport function removeTaxGroupingFromLineId(lineId) {\n    // Tax grouping is not relevant for annotations, so we remove it from the line id.\n    return buildLineId(\n        parseLineId(lineId, true).filter(([markup, model, value]) => {\n            return model !== \"account.group\";\n        })\n    );\n}\n", "/** @odoo-module **/\n\nimport { useRef, onWillRender } from \"@odoo/owl\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { WarningDialog } from \"@web/core/errors/error_dialogs\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useNestedSortable } from \"@web/core/utils/nested_sortable\";\nimport { registry } from \"@web/core/registry\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { X2ManyField, x2ManyField } from \"@web/views/fields/x2many/x2many_field\";\nimport { useOpenX2ManyRecord, useX2ManyCrud } from \"@web/views/fields/relational_utils\";\n\nexport class AccountReportListRenderer extends ListRenderer {\n    static template = \"account_reports.AccountReportList\";\n\n    setup() {\n        this.dialog = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n\n        // From ListRenderer\n        // We can't really use `super.setup()` because it expects to be used on a html table.\n        this.allColumns = this.processAllColumn(this.props.archInfo.columns, this.props.list);\n        this.keyOptionalFields = `optional_fields,${this.createViewKey()}`;\n        this.optionalActiveFields = this.computeOptionalActiveFields();\n        this.columns = this.getActiveColumns(this.props.list);\n\n        this.env.model.config.activeFields.line_ids.defaultOrderBy = [\n            {\n                \"name\": \"sequence\",\n                \"asc\": true\n            },\n            {\n                \"name\": \"id\",\n                \"asc\": true\n            }\n        ]\n\n        useNestedSortable({\n            ref: useRef(\"root\"),\n            elements: \"li.draggable\",\n            nest: true,\n            nestInterval: 10,\n            onDragStart: (ctx) => this.onDragStart(ctx),\n            onDragEnd: () => this.onDragEnd(),\n            onDrop: (ctx) => this.onDrop(ctx),\n        });\n\n        onWillRender(() => {\n            this.allColumns = this.processAllColumn(this.props.archInfo.columns, this.props.list);\n            this.columns = this.getActiveColumns(this.props.list);\n        });\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Records\n    //------------------------------------------------------------------------------------------------------------------\n    recordsDataDeepCopy(records) {\n        const fields = this.allColumns.map((column) => column.name);\n\n        return records.map((record) => {\n            let recordData = {};\n\n            for (const field of fields)\n                recordData[field] = record.data[field];\n\n            return recordData;\n        });\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Format\n    //------------------------------------------------------------------------------------------------------------------\n    formatData() {\n        let idToIndexMap = {};\n        let tree = [];\n\n        let lines = this.recordsDataDeepCopy(this.props.list.records);\n\n        for (const [index, line] of lines.entries()) {\n            line.index = index;\n            line.children = [];\n            line.descendants_count = 0;\n\n            idToIndexMap[line.id] = index;\n\n            if (line.parent_id?.[0]) {\n                let parentLine = lines[idToIndexMap[line.parent_id[0]]];\n\n                if (parentLine) {\n                    parentLine?.children.push(line);\n\n                    while (parentLine) {\n                        parentLine.descendants_count += 1;\n                        parentLine = lines[idToIndexMap[parentLine.parent_id?.[0]]];\n                    }\n                } else {\n                    // Since the parentLine doesn't exist yet. It means that this line is out of sequence.\n                    line.out_of_sequence_error = true;\n\n                    tree.push(line);\n                }\n            } else {\n                tree.push(line);\n            }\n        }\n\n        return tree;\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Placeholder\n    //------------------------------------------------------------------------------------------------------------------\n    onDragStart(ctx) {\n        function sanitize(element) {\n            if (element.nodeName === 'LI') {\n                [\"data-record_index\", \"data-record_id\", \"data-descendants_count\"].forEach((attribute) => {\n                    element.removeAttribute(attribute);\n                });\n\n                element.classList.remove(\"draggable\");\n            }\n\n            Array.from(element.childNodes).forEach((child) => {\n                sanitize(child);\n            });\n        }\n\n        const placeholder = ctx.element.cloneNode(true);\n\n        placeholder.removeAttribute(\"style\");\n        placeholder.classList.replace(\"o_dragged\", \"o_dragged_placeholder\");\n\n        sanitize(placeholder);\n\n        document.querySelector(\".o_nested_sortable_placeholder\").after(placeholder);\n    }\n\n    onDragEnd() {\n        // Clear placeholder\n        document.querySelector(\".o_dragged_placeholder\").remove();\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Nested sorting\n    //------------------------------------------------------------------------------------------------------------------\n    async setRecordParent(currentElement, parentElement) {\n        const currentRecordIndex = currentElement.dataset.record_index;\n        const parentRecordIndex = parentElement?.dataset.record_index;\n\n        // Default root element\n        let parent = false;\n\n        // parentRecordIndex is a string. It should be true with '0'.\n        if (parentRecordIndex) {\n            parent = [\n                this.props.list.records[parentRecordIndex].data.id,\n                this.props.list.records[parentRecordIndex].data.name,\n            ];\n        }\n\n        await this.props.list.records[currentRecordIndex].update({'parent_id': parent});\n    }\n\n    async setRecordHierarchy(currentElement, parentElement) {\n        const currentRecordIndex = parseInt(currentElement.dataset.record_index);\n        const parentRecordIndex = parentElement?.dataset.record_index;\n        const parentRecord = (parentRecordIndex) ? this.props.list.records[parentRecordIndex].data : null;\n\n        const hierarchyLevels = {};\n\n        if (parentRecord)\n            hierarchyLevels[parentRecord.id] = parentRecord.hierarchy_level;\n\n        const ancestors = new Set();\n\n        for (let index = currentRecordIndex; index < this.props.list.records.length; index++) {\n            const record = this.props.list.records[index];\n            const parentId = (record.data.parent_id) ? record.data.parent_id[0] : false;\n\n            if (ancestors.size && !ancestors.has(parentId))\n                break;\n\n            let parentHierarchyLevel = (record.data.parent_id) ? hierarchyLevels[record.data.parent_id[0]] : null;\n\n            if (parentHierarchyLevel != null) {\n                parentHierarchyLevel = (parentHierarchyLevel === 0) ? 1 : parentHierarchyLevel;\n                await record.update({'hierarchy_level': parentHierarchyLevel + 2});\n            } else {\n                await record.update({'hierarchy_level': 1});\n            }\n\n            ancestors.add(record.data.id);\n            hierarchyLevels[record.data.id] = record.data.hierarchy_level;\n        }\n    }\n\n    async setRecordSequence(currentElement, parentElement, previousElement, previousElementDescendantCount, nextElement) {\n        const currentRecordIndex = currentElement.dataset.record_index;\n        const currentRecordDescendantsCount = parseInt(currentElement.dataset.descendants_count);\n        const lastDescendantIndex = parseInt(currentRecordIndex) + currentRecordDescendantsCount\n        const recordsToMove = this.props.list.records.slice(currentRecordIndex, lastDescendantIndex + 1);\n\n        // We remove the element(s) we are moving\n        this.props.list.records.splice(currentRecordIndex, recordsToMove.length);\n\n        const previousRecordIndex = previousElement?.dataset.record_index;\n        const nextRecordIndex = nextElement?.dataset.record_index;\n\n        let newCurrentRecordIndex;\n\n        if (previousRecordIndex) {\n            newCurrentRecordIndex = parseInt(previousRecordIndex) + 1 + parseInt(previousElementDescendantCount);\n        } else if (nextRecordIndex) {\n            // We add the element in the first position\n            newCurrentRecordIndex = parseInt(nextRecordIndex);\n        } else {\n            // We add the element as a first child\n            newCurrentRecordIndex = parseInt(parentElement?.dataset.record_index) + 1;\n        }\n\n        // If the original position of the line we want to move is before the position we want to drop it, then we need\n        // to adjust the index because all the indexes of the lines after it have changed (we removed lines).\n        if (currentRecordIndex < newCurrentRecordIndex) {\n            newCurrentRecordIndex -= (1 + currentRecordDescendantsCount);\n        }\n\n        // We add the element(s) we are moving into the new position\n        this.props.list.records.splice(newCurrentRecordIndex, 0, ...recordsToMove);\n\n        for (const [index, record] of this.props.list.records.entries())\n            await record.update({'sequence': index + 1});\n    }\n\n    async onDrop(ctx) {\n        const parentRecordIndex = ctx.parent?.dataset.record_index;\n\n        // We can't drop a line if it's parent has a 'user_groupby`\n        if (this.props.list.records[parentRecordIndex]?.data.user_groupby)\n            return this.dialog.add(WarningDialog, {\n                message: _t(\"A line with a 'Group By' value cannot have children.\"),\n            });\n\n        // We need to save it before as it's value might change during calculations below\n        const previousElementDescendantCount = ctx.previous?.dataset.descendants_count\n\n        await this.setRecordParent(ctx.element, ctx.parent);\n        await this.setRecordHierarchy(ctx.element, ctx.parent);\n        await this.setRecordSequence(ctx.element, ctx.parent, ctx.previous, previousElementDescendantCount, ctx.next);\n    }\n\n    //------------------------------------------------------------------------------------------------------------------\n    // Delete\n    //------------------------------------------------------------------------------------------------------------------\n    onDeleteRecord(recordIndex) {\n        const currentRecordId = this.props.list.records[recordIndex].data.id\n        const nextRecordParentId = this.props.list.records[recordIndex + 1]?.data.parent_id[0]\n\n        // We check if the next line is a children of the current one\n        if (nextRecordParentId === currentRecordId)\n            return this.dialog.add(ConfirmationDialog, {\n                body: _t(\"This line and all its children will be deleted. Are you sure you want to proceed?\"),\n                confirmLabel: \"Delete\",\n                confirm: () => { this.deleteRecord(recordIndex) },\n                cancel: () => {},\n            });\n\n        this.deleteRecord(recordIndex);\n    }\n\n    deleteRecord(recordIndex) {\n        const recordToDelete = this.props.list.records[recordIndex];\n\n        const recordsToDelete = [recordToDelete];\n        const ancestors = new Set([recordToDelete.data.id]);\n\n        // We get all the children of the line we are deleting\n        for (let index = recordIndex + 1; index < this.props.list.records.length; index++) {\n            const record = this.props.list.records[index];\n\n            if (!ancestors.has(record.data.parent_id[0]))\n                break;\n\n            recordsToDelete.push(record);\n            ancestors.add(record.data.id);\n        }\n\n        for (const record of recordsToDelete)\n            this.props.list.delete(record);\n    }\n}\n\nexport class AccountReportsLinesListX2ManyField extends X2ManyField {\n    static components = {\n        ...X2ManyField.components,\n        ListRenderer: AccountReportListRenderer,\n    }\n\n    // Overrides the \"openRecord\" method to overload the save. This will force the record to be saved in the database.\n    setup() {\n        super.setup();\n\n        const { saveRecord, updateRecord } = useX2ManyCrud(\n            () => this.list,\n            this.isMany2Many\n        );\n\n        const openRecord = useOpenX2ManyRecord({\n            resModel: this.list.resModel,\n            activeField: this.activeField,\n            activeActions: this.activeActions,\n            getList: () => this.list,\n            saveRecord: async (record) => {\n                for (const [index, record] of this.props.record.data.line_ids.records.entries())\n                    record.update({'sequence': index + 1});\n\n                record.update({\n                    'sequence': this.props.record.data.line_ids.records.length,\n                });\n\n                await saveRecord(record);\n                await this.props.record.save();\n            },\n            updateRecord: updateRecord,\n            isMany2Many: this.isMany2Many,\n        });\n\n        this._openRecord = (params) => {\n            const activeElement = document.activeElement;\n\n            openRecord({\n                ...params,\n                onClose: () => {\n                    if (activeElement) {\n                        activeElement.focus();\n                    }\n                },\n            });\n        };\n    }\n}\n\nregistry.category(\"fields\").add(\"account_report_lines_list_x2many\", {\n    ...x2ManyField,\n    component: AccountReportsLinesListX2ManyField,\n});\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { Component } from \"@odoo/owl\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nexport class MoveReversed extends Component {\n    static template = \"account_asset.moveReversed\";\n    static props = {...standardFieldProps};\n}\n\nexport const moveReversed = {\n    component: MoveReversed,\n};\n\nregistry.category(\"fields\").add(\"deprec_lines_reversed\", moveReversed);\n", "import { Mutex } from \"@web/core/utils/concurrency\";\nimport { checkFileSize } from \"@web/core/utils/files\";\n\nexport class BinaryFileManager {\n    constructor(resModel, fields, parameters, context, orm, notificationService) {\n        this.resModel = resModel;\n        this.fields = [\".id\", ...fields];\n        this.parameters = parameters;\n\n        this.context = context;\n        this.orm = orm;\n        this.notificationService = notificationService;\n\n        this.maxBatchSize = this.parameters.maxBatchSize * 0.95; // 0.95 for not calculated payload overhead\n        this.delayAfterEachBatch = this.parameters.delayAfterEachBatch * 1000;\n\n        this.dataToSend = {};\n\n        this.mutex = new Mutex();\n    }\n\n    async addFile(id, field, file) {\n        let data = await this._readFile(file);\n        if (typeof data === \"string\" && data.startsWith(\"data:\")) {\n            // Remove data:image/*;base64,\n            data = data.split(\",\")[1];\n        }\n        const dataSize = data.length;\n        if (!checkFileSize(dataSize, this.notificationService)) {\n            return;\n        }\n\n        if (this.getCurrentSize() + dataSize >= this.maxBatchSize) {\n            await this.mutex.exec(async () => await this._send());\n        }\n        if (!(id in this.dataToSend)) {\n            this.dataToSend[id] = Array(this.fields.length);\n            this.dataToSend[id][0] = id;\n        }\n        const indexOfField = this.fields.indexOf(field, 1);\n        this.dataToSend[id][indexOfField] = data;\n    }\n\n    async sendLastPayload() {\n        if (Object.keys(this.dataToSend).length > 0) {\n            await this.mutex.exec(async () => await this._send());\n        }\n    }\n\n    async _send() {\n        await new Promise((resolve) => {\n            setTimeout(resolve, this.delayAfterEachBatch);\n        });\n        const data = Object.values(this.dataToSend);\n        this.dataToSend = {};\n        const context = {\n            ...this.context,\n            import_file: true,\n            tracking_disable: this.parameters.tracking_disable,\n            name_create_enabled_fields: this.parameters.name_create_enabled_fields || {},\n            import_set_empty_fields: this.parameters.import_set_empty_fields || [],\n            import_skip_records: this.parameters.import_skip_records || [],\n        };\n        let res;\n        try {\n            res = await this.orm.call(this.resModel, \"load\", [], {\n                fields: this.fields,\n                data,\n                context,\n            });\n        } catch (error) {\n            console.error(error);\n            return { error };\n        }\n        return res;\n    }\n\n    _readFile(file) {\n        return new Promise((resolve, reject) => {\n            const reader = new FileReader();\n            reader.onerror = (event) => reject(event);\n            reader.onabort = (event) => reject(event);\n            reader.onload = (event) => resolve(event.target.result);\n            reader.readAsDataURL(file);\n        });\n    }\n\n    getCurrentSize() {\n        return JSON.stringify(this.dataToSend).length;\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Component, onWillStart, onMounted, useRef, useState } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { useFileUploader } from \"@web/core/utils/files\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { FileInput } from \"@web/core/file_input/file_input\";\nimport { useDropzone } from \"@web/core/dropzone/dropzone_hook\";\nimport { useImportModel } from \"../import_model\";\nimport { ImportDataContent } from \"../import_data_content/import_data_content\";\nimport { ImportDataProgress } from \"../import_data_progress/import_data_progress\";\nimport { ImportDataSidepanel } from \"../import_data_sidepanel/import_data_sidepanel\";\nimport { Layout } from \"@web/search/layout\";\nimport { router } from \"@web/core/browser/router\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\nimport { DocumentationLink } from \"@web/views/widgets/documentation_link/documentation_link\";\n\nexport class ImportAction extends Component {\n    static template = \"ImportAction\";\n    static nextId = 1;\n    static components = {\n        FileInput,\n        ImportDataContent,\n        ImportDataSidepanel,\n        Layout,\n        DocumentationLink,\n    };\n    static props = { ...standardActionServiceProps };\n\n    setup() {\n        this.notification = useService(\"notification\");\n        this.orm = useService(\"orm\");\n        this.env.config.setDisplayName(this.props.action.name || _t(\"Import a File\"));\n        // this.props.action.params.model is there for retro-compatiblity issues\n        this.resModel = this.props.action.params.model || this.props.action.params.active_model;\n        if (this.resModel) {\n            this.props.updateActionState({ active_model: this.resModel });\n        }\n        this.model = useImportModel({\n            env: this.env,\n            resModel: this.resModel,\n            context: this.props.action.params.context || {},\n            orm: this.orm,\n        });\n\n        this.state = useState({\n            filename: undefined,\n            fileLength: 0,\n            importMessages: [],\n            importProgress: {\n                value: 0,\n                step: 1,\n            },\n            isPaused: false,\n            previewError: \"\",\n        });\n\n        this.uploadFiles = useFileUploader();\n        useDropzone(useRef(\"root\"), async event => {\n            const { files } = event.dataTransfer;\n            if (files.length === 0) {\n                this.notification.add(_t(\"Please upload an Excel (.xls or .xlsx) or .csv file to import.\"), {\n                    type: \"danger\",\n                });\n            } else if (files.length > 1) {\n                this.notification.add(_t(\"Please upload a single file.\"), {\n                    type: \"danger\",\n                });\n            } else {\n                const file = files[0];\n                const isValidFile = file.name.endsWith(\".csv\")\n                                 || file.name.endsWith(\".xls\")\n                                 || file.name.endsWith(\".xlsx\");\n                if (!isValidFile) {\n                    this.notification.add(_t(\"Please upload an Excel (.xls or .xlsx) or .csv file to import.\"), {\n                        type: \"danger\",\n                    });\n                } else {\n                    await this.uploadFiles(this.uploadFilesRoute, {\n                        csrf_token: odoo.csrf_token,\n                        ufile: [file],\n                        model: this.resModel,\n                        id: this.model.id,\n                    });\n                    this.handleFilesUpload([file]);\n                }\n            }\n        });\n\n        onWillStart(() => this.model.init());\n        onMounted(() => this.enter());\n    }\n\n    enter() {\n        const newState = { action: \"import\", model: this.resModel };\n        router.pushState(newState, { replace: true });\n    }\n\n    exit(resIds) {\n        this.env.config.historyBack();\n    }\n\n    get display() {\n        return {\n            controlPanel: {},\n        };\n    }\n\n    get importTemplates() {\n        return this.model.importTemplates;\n    }\n\n    get uploadFilesRoute () {\n        return \"/base_import/set_file\";\n    }\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    get formattingOptions() {\n        return this.model.formattingOptions;\n    }\n\n    get totalToImport() {\n        return this.state.fileLength - parseInt(this.importOptions.skip);\n    }\n\n    get totalSteps() {\n        return this.isBatched ? Math.ceil(this.totalToImport / this.importOptions.limit) : 1;\n    }\n\n    get importOptions() {\n        return this.model.importOptions;\n    }\n\n    get isPreviewing() {\n        return this.state.filename !== undefined;\n    }\n\n    // Activate the batch configuration panel only if the file length > 100. (In order to let the user choose\n    // the batch size even for medium size file. Could be useful to reduce the batch size for complex models).\n    get isBatched() {\n        return this.state.fileLength > 100;\n    }\n\n    async onOptionChanged(name, value, fieldName = null) {\n        this.model.block();\n        const result = await this.model.setOption(name, value, fieldName);\n        if (result) {\n            const { res, error } = result;\n            if (!error && res.file_length) {\n                this.state.fileLength = res.file_length;\n            }\n        }\n        this.model.unblock();\n    }\n\n    async reload() {\n        this.model.block();\n        await this.model.updateData();\n        this.model.unblock();\n    }\n\n    //--------------------------------------------------------------------------\n    // File\n    //--------------------------------------------------------------------------\n\n    async handleFilesUpload(files) {\n        if (!files || files.length <= 0) {\n            return;\n        }\n\n        this.state.filename = files[0].name;\n        this.state.importMessages = [];\n\n        this.model.block(_t(\"Loading file...\"));\n        const { res, error } = await this.model.updateData(true);\n\n        if (error) {\n            this.state.previewError = error;\n        } else {\n            this.state.fileLength = res.file_length;\n            this.state.previewError = undefined;\n        }\n        this.model.unblock();\n    }\n\n    async handleImport(isTest = true) {\n        const message = isTest ? _t(\"Testing\") : _t(\"Importing\");\n\n        let blockComponent;\n        if (this.isBatched) {\n            blockComponent = {\n                class: ImportDataProgress,\n                props: {\n                    stopImport: () => this.stopImport(),\n                    totalSteps: this.totalSteps,\n                    importProgress: this.state.importProgress,\n                },\n            };\n        }\n\n        this.model.block(message, blockComponent);\n\n        let res = { ids: [] };\n        try {\n            const data = await this.model.executeImport(\n                isTest,\n                this.totalSteps,\n                this.state.importProgress\n            );\n            res = data.res;\n        } finally {\n            this.model.unblock();\n        }\n\n        if (!isTest && res.nextrow) {\n            this.state.isPaused = true;\n        }\n\n        if (!isTest && res.ids.length) {\n            this.notification.add(_t(\"%s records successfully imported\", res.ids.length), {\n                type: \"success\",\n            });\n            this.exit(res.ids);\n        }\n    }\n\n    stopImport() {\n        this.model.stopImport();\n    }\n\n    //--------------------------------------------------------------------------\n    // Fields\n    //--------------------------------------------------------------------------\n\n    onFieldChanged(column, fieldInfo) {\n        this.model.setColumnField(column, fieldInfo);\n    }\n\n    isFieldSet(column) {\n        return column.fieldInfo != null;\n    }\n\n    get hasBinaryFields() {\n        return this.model.columns.some((column) => column.fieldInfo?.type === \"binary\");\n    }\n}\n\nregistry.category(\"actions\").add(\"import\", ImportAction);\n", "/** @odoo-module **/\n\nimport { Component } from \"@odoo/owl\";\n\nexport class ImportBlockUI extends Component {\n    static props = {\n        message: { type: String, optional: true },\n        blockComponent: { type: Object, optional: true },\n    };\n    static template = \"base_import.BlockUI\";\n}\n", "/** @odoo-module **/\n\nimport { Component, useState } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class ImportDataColumnError extends Component {\n    static template = \"ImportDataColumnError\";\n    static props = {\n        errors: { type: Array },\n        fieldInfo: { type: Object },\n        resultNames: { type: Array },\n    };\n\n    setup() {\n        this.action = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.state = useState({\n            isExpanded: false,\n            moreInfoContent: undefined,\n        });\n    }\n    get moreInfo() {\n        const moreInfoObjects = this.props.errors.map((error) => error.moreinfo);\n        return moreInfoObjects.length && moreInfoObjects[0];\n    }\n    isErrorVisible(index) {\n        return this.state.isExpanded || index < 3;\n    }\n    onMoreInfoClicked() {\n        const moreInfo = this.moreInfo;\n        if (this.state.moreInfoContent) {\n            this.state.moreInfoContent = undefined;\n        } else if (moreInfo instanceof Array) {\n            this.state.moreInfoContent = moreInfo;\n        } else {\n            this.action.doAction(moreInfo);\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { Component } from \"@odoo/owl\";\nimport { SelectMenu } from \"@web/core/select_menu/select_menu\";\nimport { ImportDataColumnError } from \"../import_data_column_error/import_data_column_error\";\nimport { ImportDataOptions } from \"../import_data_options/import_data_options\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class ImportDataContent extends Component {\n    static template = \"ImportDataContent\";\n    static components = {\n        ImportDataColumnError,\n        ImportDataOptions,\n        SelectMenu,\n    };\n    static props = {\n        columns: { type: Array },\n        isFieldSet: { type: Function },\n        onOptionChanged: { type: Function },\n        onFieldChanged: { type: Function },\n        options: { type: Object },\n        importMessages: { type: Object },\n        previewError: { type: String, optional: true },\n    };\n\n    getGroups(column) {\n        const groups = [\n            { choices: this.makeChoices(column.fields.basic) },\n            { choices: this.makeChoices(column.fields.suggested), label: _t(\"Suggested Fields\") },\n            {\n                choices: this.makeChoices(column.fields.additional),\n                label:\n                    column.fields.suggested.length > 0\n                        ? _t(\"Additional Fields\")\n                        : _t(\"Standard Fields\"),\n            },\n            { choices: this.makeChoices(column.fields.relational), label: _t(\"Relation Fields\") },\n        ];\n        return groups;\n    }\n\n    makeChoices(fields) {\n        return fields.map((field) => ({\n            label: field.label,\n            value: field.fieldPath,\n            iconClass: `o_import_field_icon_${field.type}`,\n        }));\n    }\n\n    getTooltipDetails(field) {\n        return JSON.stringify({\n            resModel: field.model_name,\n            debug: true,\n            field: {\n                name: field.name,\n                label: field.string,\n                type: field.type,\n            },\n        });\n    }\n\n    getTooltip(column) {\n        const displayCount = 5;\n        if (column.previews.length > displayCount) {\n            return JSON.stringify({\n                lines: [\n                    ...column.previews.slice(0, displayCount - 1),\n                    `(+${column.previews.length - displayCount + 1})`,\n                ],\n            });\n        } else {\n            return JSON.stringify({ lines: column.previews.slice(0, displayCount) });\n        }\n    }\n\n    getErrorMessageClass(messages, type, index) {\n        return `alert alert-${type} m-0 p-2 ${index === messages.length - 1 ? \"\" : \"mb-2\"}`;\n    }\n\n    getCommentClass(column, comment, index) {\n        return `alert-${comment.type} ${index < column.comments.length - 1 ? \"mb-2\" : \"mb-0\"}`;\n    }\n\n    onFieldChanged(column, fieldPath) {\n        const fields = [\n            ...column.fields.basic,\n            ...column.fields.suggested,\n            ...column.fields.additional,\n            ...column.fields.relational,\n        ];\n        const fieldInfo = fields.find((f) => f.fieldPath === fieldPath);\n        this.props.onFieldChanged(column, fieldInfo);\n    }\n}\n", "/** @odoo-module **/\n\nimport { Component, useState, onWillStart } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class ImportDataOptions extends Component {\n    static template = \"ImportDataOptions\";\n    static props = {\n        importOptions: { type: Object, optional: true },\n        fieldInfo: { type: Object },\n        onOptionChanged: { type: Function },\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.state = useState({\n            options: [],\n        });\n        this.currentModel = this.props.fieldInfo.comodel_name || this.props.fieldInfo.model_name;\n        onWillStart(async () => {\n            this.state.options = await this.loadOptions();\n        });\n    }\n    get isVisible() {\n        return [\"many2one\", \"many2many\", \"selection\", \"boolean\"].includes(\n            this.props.fieldInfo.type\n        );\n    }\n    async loadOptions() {\n        const options = [[\"prevent\", _t(\"Prevent import\")]];\n        if (this.props.fieldInfo.type === \"boolean\") {\n            options.push([\"false\", _t(\"Set to: False\")]);\n            options.push([\"true\", _t(\"Set to: True\")]);\n            !this.props.fieldInfo.required &&\n                options.push([\"import_skip_records\", _t(\"Skip record\")]);\n        }\n        if ([\"many2one\", \"many2many\", \"selection\"].includes(this.props.fieldInfo.type)) {\n            if (!this.props.fieldInfo.required) {\n                options.push([\"import_set_empty_fields\", _t(\"Set value as empty\")]);\n                options.push([\"import_skip_records\", _t(\"Skip record\")]);\n            }\n            if (this.props.fieldInfo.type === \"selection\") {\n                const fields = await this.orm.call(this.currentModel, \"fields_get\");\n                const selection = fields[this.props.fieldInfo.name].selection.map((opt) => [\n                    opt[0],\n                    _t(\"Set to: %s\", opt[1]),\n                ]);\n                options.push(...selection);\n            } else {\n                options.push([\"name_create_enabled_fields\", _t(\"Create new values\")]);\n            }\n        }\n        return options;\n    }\n    onSelectionChanged(ev) {\n        if (\n            [\n                \"name_create_enabled_fields\",\n                \"import_set_empty_fields\",\n                \"import_skip_records\",\n            ].includes(ev.target.value)\n        ) {\n            this.props.onOptionChanged(\n                ev.target.value,\n                ev.target.value,\n                this.props.fieldInfo.fieldPath\n            );\n        } else {\n            const value = {\n                fallback_value: ev.target.value,\n                field_model: this.currentModel,\n                field_type: this.props.fieldInfo.type,\n            };\n            this.props.onOptionChanged(\"fallback_values\", value, this.props.fieldInfo.fieldPath);\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { Component, useEffect, useState } from \"@odoo/owl\";\n\nexport class ImportDataProgress extends Component {\n    static template = \"ImportDataProgress\";\n    static props = {\n        importProgress: { type: Object },\n        stopImport: { type: Function },\n        totalSteps: { type: Number },\n    };\n\n    setup() {\n        this.timer = undefined;\n        this.timeStart = Date.now();\n        this.state = useState({\n            isInterrupted: false,\n            timeLeft: null,\n        });\n\n        useEffect(\n            () => {\n                this.updateTimer();\n                return () => {\n                    clearInterval(this.timer);\n                };\n            },\n            () => []\n        );\n    }\n\n    get minutesLeft() {\n        return this.state.timeLeft.toFixed(2);\n    }\n\n    get secondsLeft() {\n        return Math.round(this.state.timeLeft * 60);\n    }\n\n    interrupt() {\n        this.state.isInterrupted = true;\n        this.props.stopImport();\n    }\n\n    updateTimer() {\n        if (this.timer) {\n            clearInterval(this.timer);\n        }\n        this.state.timeLeft =\n            ((Date.now() - this.timeStart) *\n                ((100 - this.props.importProgress.value) / this.props.importProgress.value)) /\n            60000;\n        this.timer = setInterval(() => this.updateTimer(), 1000);\n    }\n}\n", "/** @odoo-module **/\n\nimport { Component } from \"@odoo/owl\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { DocumentationLink } from \"@web/views/widgets/documentation_link/documentation_link\";\n\nexport class ImportDataSidepanel extends Component {\n    static template = \"ImportDataSidepanel\";\n    static components = { CheckBox, DocumentationLink };\n    static props = {\n        filename: { type: String },\n        formattingOptions: { type: Object, optional: true },\n        options: { type: Object },\n        importTemplates: { type: Array, optional: true },\n        isBatched: { type: Boolean, optional: true },\n        onOptionChanged: { type: Function },\n        onReload: { type: Function },\n        hasBinaryFields: { type: Boolean },\n        binaryFilesParams: { type: Object },\n        onBinaryFilesParamsChanged: { type: Function },\n    };\n\n    get fileName() {\n        return this.props.filename.split(\".\")[0];\n    }\n\n    get fileExtension() {\n        return \".\" + this.props.filename.split(\".\").pop();\n    }\n\n    getOptionValue(name) {\n        if (name === \"skip\") {\n            return (this.props.options.skip + 1).toString();\n        }\n        return this.props.options[name].toString();\n    }\n\n    setOptionValue(name, value) {\n        this.props.onOptionChanged(name, isNaN(parseFloat(value)) ? value : Number(value));\n    }\n\n    // Start at row 1 = skip 0 lines\n    onLimitChange(ev) {\n        this.props.onOptionChanged(\"skip\", ev.target.value ? ev.target.value - 1 : 0);\n    }\n\n    get binaryFilesLabel() {\n        const files = this.props.binaryFilesParams.binaryFiles.value;\n        const number = Object.keys(files).length;\n        if (number > 0) {\n            return _t(\"%(number)s file(s) selected\", { number });\n        }\n        return _t(\"No file selected\");\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { checkFileSize, DEFAULT_MAX_FILE_SIZE } from \"@web/core/utils/files\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { pick } from \"@web/core/utils/objects\";\nimport { groupBy, sortBy } from \"@web/core/utils/arrays\";\nimport { memoize } from \"@web/core/utils/functions\";\nimport { session } from \"@web/session\";\nimport { useState } from \"@odoo/owl\";\nimport { ImportBlockUI } from \"./import_block_ui\";\nimport { BinaryFileManager } from \"./binary_file_manager\";\n\nconst mainComponentRegistry = registry.category(\"main_components\");\n\nconst strftimeFormatTable = {\n    d: \"w\",\n    DD: \"d\",\n    ddd: \"a\",\n    dddd: \"A\",\n    DDDD: \"j\",\n    ww: \"U\",\n    WW: \"W\",\n    mm: \"M\",\n    MM: \"m\",\n    MMM: \"b\",\n    MMMM: \"B\",\n    YYYY: \"Y\",\n    YY: \"y\",\n    ss: \"S\",\n    hh: \"h\",\n    HH: \"H\",\n    A: \"p\",\n};\n\n/**\n * Convert a human readable format to Python strftime format. In case\n * no corresponding format is supported, a similar fallback is given\n * from the list of other supported formatting value.\n *\n * @param {string} value original Luxon format\n * @returns {string} valid strftime format\n */\nconst humanToStrftimeFormat = memoize(function humanToStrftimeFormat(value) {\n    const regex = /(dddd|ddd|dd|d|mmmm|mmm|mm|ww|yyyy|yy|hh|ss|a)/gi;\n    return value.replace(regex, (value) => {\n        if (strftimeFormatTable[value]) {\n            return \"%\" + strftimeFormatTable[value];\n        }\n        return (\n            \"%\" +\n            (strftimeFormatTable[value.toLowerCase()] || strftimeFormatTable[value.toUpperCase()])\n        );\n    });\n});\n\nconst strftimeToHumanFormat = memoize(function strftimeToHumanFormat(value) {\n    Object.entries(strftimeFormatTable).forEach(([k, v]) => {\n        value = value.replace(`%${v}`, k);\n    });\n    return value;\n});\n\n/**\n * -------------------------------------------------------------------------\n * Base Import Business Logic\n * -------------------------------------------------------------------------\n *\n * Handles mapping and updating the preview data of the csv/excel files to be\n * used in the different base_import components.\n *\n * When uploading a file some \"preview data\" is returned by the backend, this\n * data consist of the different columns of the file and the odoo fields which\n * these columns can be mapped to.\n *\n * Only a small selection of the lines are returned so the user can get an idea\n * of how to correctly map the columns. *(this is why it is refered as \"preview\n * data\")*\n *\n */\nexport class BaseImportModel {\n    constructor({ env, resModel, context, orm }) {\n        this.id = 1;\n        this.env = env;\n        this.orm = orm;\n        this.handleInterruption = false;\n\n        this.resModel = resModel;\n        this.context = context || {};\n\n        this.fields = [];\n        this.columns = [];\n        this.importMessages = [];\n        this._importOptions = {};\n\n        this.importTemplates = [];\n\n        this.formattingOptionsValues = this._getCSVFormattingOptions();\n\n        this.importOptionsValues = {\n            ...this.formattingOptionsValues,\n            advanced: {\n                reloadParse: true,\n                value: true,\n            },\n            has_headers: {\n                reloadParse: true,\n                value: true,\n            },\n            keep_matches: {\n                value: false,\n            },\n            limit: {\n                value: 2000,\n            },\n            sheets: {\n                value: [],\n            },\n            sheet: {\n                label: _t(\"Selected Sheet:\"),\n                reloadParse: true,\n                value: \"\",\n            },\n            skip: {\n                value: 0,\n            },\n            tracking_disable: {\n                value: true,\n            },\n        };\n\n        const maxUploadSize = session.max_file_upload_size || DEFAULT_MAX_FILE_SIZE;\n        this.binaryFilesParams = {\n            binaryFiles: {\n                value: {},\n            },\n            maxSizePerBatch: {\n                help: _t(\"Defines how many megabytes can be imported in each batch import\"),\n                value: 10,\n                max: Math.round(maxUploadSize / 1024 / 1024),\n                min: 0,\n            },\n            delayAfterEachBatch: {\n                help: _t(\n                    \"After each batch import, this delay is applied to avoid unthrottled calls\"\n                ),\n                value: 1,\n                min: 1,\n            },\n        };\n\n        this.fieldsToHandle = {};\n\n        this.notificationService = useService(\"notification\");\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    get formattingOptions() {\n        return pick(this.importOptionsValues, ...Object.keys(this.formattingOptionsValues));\n    }\n\n    /**\n     * This getter returns the current values pf the options, formatted to match the\n     * server API (date and datetime options should be Python strftime formatted)\n     */\n    get formattedImportOptions() {\n        const options = this.importOptions;\n        options.date_format = humanToStrftimeFormat(options.date_format);\n        options.datetime_format = humanToStrftimeFormat(options.datetime_format);\n        return options;\n    }\n\n    get importOptions() {\n        const tempImportOptions = {\n            import_skip_records: [],\n            import_set_empty_fields: [],\n            fallback_values: {},\n            name_create_enabled_fields: {},\n        };\n        for (const [name, option] of Object.entries(this.importOptionsValues)) {\n            tempImportOptions[name] = option.value;\n        }\n\n        for (const key in this.fieldsToHandle) {\n            const value = this.fieldsToHandle[key];\n            if (value) {\n                if (value.optionName === \"import_skip_records\") {\n                    tempImportOptions.import_skip_records.push(key);\n                } else if (value.optionName === \"import_set_empty_fields\") {\n                    tempImportOptions.import_set_empty_fields.push(key);\n                } else if (value.optionName === \"name_create_enabled_fields\") {\n                    tempImportOptions.name_create_enabled_fields[key] = true;\n                } else if (value.optionName === \"fallback_values\") {\n                    tempImportOptions.fallback_values[key] = value.value;\n                }\n            }\n        }\n\n        this._importOptions = tempImportOptions;\n        return tempImportOptions;\n    }\n\n    set importOptions(options) {\n        for (const key in options) {\n            this.importOptionsValues[key].value = options[key];\n        }\n    }\n\n    /**\n     * A custom BlockUI is required to add the progress bar or text when blocking\n     * the UI, without modifying the core ui service to handle a generic use case\n     */\n    block(message, blockComponent) {\n        mainComponentRegistry.add(\n            \"ImportBlockUI\",\n            {\n                Component: ImportBlockUI,\n                props: {\n                    blockComponent,\n                    message,\n                },\n            },\n            { force: true }\n        );\n    }\n\n    unblock() {\n        mainComponentRegistry.remove(\"ImportBlockUI\");\n    }\n\n    async init() {\n        [this.importTemplates, this.id] = await Promise.all([\n            this.orm.call(this.resModel, \"get_import_templates\", [], {\n                context: this.context,\n            }),\n            this.orm.call(\"base_import.import\", \"create\", [{ res_model: this.resModel }]),\n        ]);\n    }\n\n    async executeImport(isTest = false, totalSteps, importProgress) {\n        this.handleInterruption = false;\n        this._updateComments();\n        this.importMessages = [];\n\n        const startRow = this.importOptions.skip;\n        const importRes = {\n            ids: [],\n            fields: this.columns.map((e) => Boolean(e.fieldInfo) && e.fieldInfo.fieldPath),\n            columns: this.columns.map((e) => e.name.trim().toLowerCase()),\n            hasError: false,\n        };\n\n        for (let i = 1; i <= totalSteps; i++) {\n            if (this.handleInterruption) {\n                if (importRes.hasError || isTest) {\n                    importRes.nextrow = startRow;\n                    this.setOption(\"skip\", startRow);\n                }\n                break;\n            }\n\n            const error = await this._executeImportStep(isTest, importRes);\n            if (error) {\n                const errorData = error.data || {};\n                const message = errorData.arguments && (errorData.arguments[1] || errorData.arguments[0])\n                    || _t(\"An unknown issue occurred during import (possibly lost connection, data limit exceeded or memory limits exceeded). Please retry in case the issue is transient. If the issue still occurs, try to split the file rather than import it at once.\");\n\n                if (error.message) {\n                    this._addMessage(\"danger\", [error.message, message]);\n                } else {\n                    this._addMessage(\"danger\", [message]);\n                }\n\n                importRes.hasError = true;\n                break;\n            }\n\n            if (importProgress) {\n                importProgress.step = i;\n                importProgress.value = Math.round((100 * (i - 1)) / totalSteps);\n            }\n        }\n\n        if (!importRes.hasError) {\n            if (importRes.nextrow) {\n                this._addMessage(\"warning\", [\n                    _t(\n                        \"Click 'Resume' to proceed with the import, resuming at line %s.\",\n                        importRes.nextrow + 1\n                    ),\n                    _t(\"You can test or reload your file before resuming the import.\"),\n                ]);\n            }\n            if (isTest) {\n                this._addMessage(\"info\", [_t(\"Everything seems valid.\")]);\n            }\n        } else {\n            importRes.nextrow = startRow;\n        }\n        return { res: importRes };\n    }\n\n    /**\n     * Ask the server for the parsing preview\n     * and update the data accordingly.\n     */\n    async updateData(fileChanged = false) {\n        if (fileChanged) {\n            this.importOptionsValues.sheet.value = \"\";\n        }\n        this.importMessages = [];\n\n        const res = await this.orm.call(\"base_import.import\", \"parse_preview\", [\n            this.id,\n            this.formattedImportOptions,\n        ]);\n\n        if (!res.error) {\n            res.options.date_format = strftimeToHumanFormat(res.options.date_format);\n            res.options.datetime_format = strftimeToHumanFormat(res.options.datetime_format);\n            this._onLoadSuccess(res);\n        } else {\n            this._onLoadError();\n        }\n        return { res, error: res.error };\n    }\n\n    async setOption(optionName, value, fieldName) {\n        if (fieldName) {\n            this.fieldsToHandle[fieldName] = {\n                optionName,\n                value,\n            };\n            return;\n        }\n        this.importOptionsValues[optionName].value = value;\n        if (this.importOptionsValues[optionName].reloadParse) {\n            return this.updateData();\n        }\n    }\n\n    onBinaryFilesParamsChanged(parameterName, value) {\n        if (parameterName === \"binaryFiles\") {\n            const files = {};\n            for (const file of value) {\n                if (checkFileSize(file.size, this.notificationService)) {\n                    files[file.name] = file;\n                }\n            }\n            value = files;\n        }\n        this.binaryFilesParams[parameterName].value = value;\n    }\n\n    setColumnField(column, fieldInfo) {\n        column.fieldInfo = fieldInfo;\n        this._updateComments(column);\n    }\n\n    isColumnFieldSet(column) {\n        return column.fieldInfo != null;\n    }\n\n    /*\n     * We must wait the current iteration of execute_import to conclude and it\n     * will stop at the start of the next batch with handleInterruption\n     */\n    stopImport() {\n        this.handleInterruption = true;\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    _addMessage(type, lines) {\n        const importMsgs = this.importMessages;\n        importMsgs.push({\n            type: type.replace(\"error\", \"danger\"),\n            lines,\n        });\n        this.importMessages = importMsgs;\n    }\n\n    async _executeImportStep(isTest, importRes) {\n        const importArgs = [\n            this.id,\n            importRes.fields,\n            importRes.columns,\n            this.formattedImportOptions,\n        ];\n        const { ids, messages, nextrow, name, error, binary_filenames } = await this._callImport(\n            isTest,\n            importArgs\n        );\n\n        // Handle server errors\n        if (error) {\n            return error;\n        }\n\n        if (ids) {\n            importRes.ids = importRes.ids.concat(ids);\n        }\n\n        // Handle import errors\n        if (messages && messages.length) {\n            importRes.hasError = true;\n            this.stopImport();\n            if (this._handleImportErrors(messages, name)) {\n                return false;\n            }\n        }\n\n        // Push local image to records\n        await this._pushLocalImageToRecords(ids, binary_filenames, isTest);\n\n        // Check if we should continue\n        if (nextrow) {\n            this.setOption(\"skip\", nextrow);\n            importRes.nextrow = nextrow;\n        } else {\n            // Falsy `nextrow` signals there's nothing left to import\n            this.stopImport();\n        }\n        return false;\n    }\n\n    async _pushLocalImageToRecords(ids, binaryFilenames, isTest) {\n        if (typeof binaryFilenames === \"object\") {\n            const parameters = {\n                tracking_disable: this.importOptions.tracking_disable,\n                delayAfterEachBatch: this.binaryFilesParams.delayAfterEachBatch.value,\n                maxBatchSize: this.binaryFilesParams.maxSizePerBatch.value * 1024 * 1024,\n            };\n\n            if (!this.binaryFilesParams.binaryFiles) {\n                return;\n            }\n            const binaryFiles = this.binaryFilesParams.binaryFiles.value;\n            const fields = Object.keys(binaryFilenames);\n            const binaryFileManager = new BinaryFileManager(\n                this.resModel,\n                fields,\n                parameters,\n                this.context,\n                this.orm,\n                this.notificationService\n            );\n            for (let rowIndex = 0; rowIndex < ids.length; rowIndex++) {\n                const id = ids[rowIndex];\n                for (const field of fields) {\n                    const fileName = binaryFilenames[field][rowIndex];\n                    if (!fileName) {\n                        continue;\n                    }\n                    if (fileName in binaryFiles) {\n                        const file = binaryFiles[fileName];\n                        if (!file || isTest) {\n                            continue;\n                        }\n                        await binaryFileManager.addFile(id, field, file);\n                    }\n                }\n            }\n            if (!isTest) {\n                await binaryFileManager.sendLastPayload();\n            }\n        }\n    }\n\n    async _callImport(dryrun, args) {\n        try {\n            const res = await this.orm.silent.call(\"base_import.import\", \"execute_import\", args, {\n                dryrun,\n                context: {\n                    ...this.context,\n                    tracking_disable: this.importOptions.tracking_disable,\n                },\n            });\n            return res;\n        } catch (error) {\n            // This pattern isn't optimal but it is need to have\n            // similar behaviours as in legacy. That is, catching\n            // all import errors and showing them inside the top\n            // \"messages\" area.\n            return { error };\n        }\n    }\n\n    _handleImportErrors(messages, name) {\n        if (messages[0].not_matching_error) {\n            this._addMessage(messages[0].type, [messages[0].message]);\n            return true;\n        }\n\n        const sortedMessages = this._groupErrorsByField(messages);\n        if (sortedMessages[0]) {\n            this._addMessage(sortedMessages[0].type, [sortedMessages[0].message]);\n            delete sortedMessages[0];\n        } else {\n            this._addMessage(\"danger\", [_t(\"The file contains blocking errors (see below)\")]);\n        }\n\n        for (const [columnFieldId, errors] of Object.entries(sortedMessages)) {\n            // Handle errors regarding specific colums.\n            const column = this.columns.find(\n                (e) => e.fieldInfo && e.fieldInfo.fieldPath === columnFieldId\n            );\n            if (column) {\n                column.resultNames = name;\n                column.errors = errors;\n            } else {\n                for (const error of errors) {\n                    // Handle errors regarding specific records.\n                    if (error.record !== undefined) {\n                        this._addMessage(\"danger\", [\n                            error.rows.from === error.rows.to\n                                ? _t('Error at row %(row)s: \"%(error)s\"', {\n                                      row: error.record,\n                                      error: error.message,\n                                  })\n                                : _t(\"%s at multiple rows\", error.message),\n                        ]);\n                    }\n                    // Handle global errors.\n                    else {\n                        this._addMessage(\"danger\", [error.message]);\n                    }\n                }\n            }\n        }\n    }\n\n    _groupErrorsByField(messages) {\n        const groupedErrors = {};\n        const errorsByMessage = groupBy(this._sortErrors(messages), (f) => f.message || \"0\");\n        for (const [message, errors] of Object.entries(errorsByMessage)) {\n            if (!message.record) {\n                const foundError = errors.find((e) => e.record === undefined);\n                if (foundError) {\n                    groupedErrors[0] = foundError;\n                    continue;\n                }\n            }\n\n            errors[0].rows.to = errors[errors.length - 1].rows.to;\n            const fieldId = errors[0].field_path ? errors[0].field_path.join(\"/\") : errors[0].field;\n            if (groupedErrors[fieldId]) {\n                groupedErrors[fieldId].push(errors[0]);\n            } else {\n                groupedErrors[fieldId] = [errors[0]];\n            }\n        }\n        return groupedErrors;\n    }\n\n    _sortErrors(messages) {\n        return sortBy(messages, (e) => [\"error\", \"warning\", \"info\"].indexOf(e.priority));\n    }\n\n    /**\n     * On the preview data succesfuly loaded, update the\n     * import options, columns and messages.\n     * @param {*} res\n     */\n    _onLoadSuccess(res) {\n        // Set options\n        for (const key in res.options) {\n            if (this.importOptionsValues[key]) {\n                this.importOptionsValues[key].value = res.options[key];\n            }\n        }\n\n        if (!res.fields.length) {\n            this.importOptionsValues.advanced.value = res.advanced_mode;\n        }\n\n        this.fields = res.fields;\n        this.columns = this._getColumns(res);\n\n        // Set import messages\n        if (res.headers.length === 1) {\n            this._addMessage(\"warning\", [\n                _t(\n                    \"A single column was found in the file, this often means the file separator is incorrect.\"\n                ),\n            ]);\n        }\n\n        this._updateComments();\n    }\n\n    _onLoadError() {\n        this.columns = [];\n        this.importMessages = [];\n    }\n\n    _getColumns(res) {\n        function getId(res, index) {\n            return res.matches && index in res.matches && res.matches[index].length > 0\n                ? res.matches[index].join(\"/\")\n                : undefined;\n        }\n\n        if (this.importOptions.has_headers && res.headers && res.preview.length > 0) {\n            return res.headers.flatMap((header, index) => {\n                return this._createColumn(\n                    res,\n                    getId(res, index),\n                    header,\n                    index,\n                    res.preview[index],\n                    res.preview[index][0]\n                );\n            });\n        } else if (res.preview && res.preview.length >= 2) {\n            return res.preview.flatMap((preview, index) =>\n                this._createColumn(\n                    res,\n                    preview[0],\n                    this.importOptions.has_headers ? preview[0] : preview.join(\", \"),\n                    index,\n                    preview,\n                    preview[1]\n                )\n            );\n        }\n        return [];\n    }\n\n    _createColumn(res, id, name, index, previews, preview) {\n        const fields = this._getFields(res, index);\n        return {\n            id,\n            name,\n            preview,\n            previews,\n            fields,\n            fieldInfo: this._findField(fields, id),\n            comments: [],\n            errors: [],\n        };\n    }\n\n    _findField(fields, id) {\n        return Object.entries(fields)\n            .flatMap((e) => e[1])\n            .find((field) => field.fieldPath === id);\n    }\n\n    /**\n     * Sort fields into their respective categories, namely:\n     * - Basic => Only the ID field\n     * - Suggested => Non-relational fields from the header\"s types\n     * - Additional => Non-relational fields of any other type\n     * - Relational => Relational fields\n     * @param {*} res\n     */\n    _getFields(res, index) {\n        const advanced = this.importOptionsValues.advanced.value;\n        const fields = {\n            basic: [],\n            suggested: [],\n            additional: [],\n            relational: [],\n        };\n\n        function isRegular(subfields) {\n            return (\n                !subfields ||\n                subfields.length === 0 ||\n                (subfields.length === 2 &&\n                    subfields[0].name === \"id\" &&\n                    subfields[1].name === \".id\")\n            );\n        }\n\n        function hasType(types, field) {\n            return types && types.indexOf(field.type) !== -1;\n        }\n\n        const sortSingleField = (field, ancestors, collection, types) => {\n            ancestors.push(field);\n            field.fieldPath = ancestors.map((f) => f.name).join(\"/\");\n            field.label = ancestors.map((f) => f.string).join(\" / \");\n\n            // Get field respective category\n            if (!collection) {\n                if (field.name === \"id\") {\n                    collection = fields.basic;\n                } else if (isRegular(field.fields)) {\n                    collection = hasType(types, field) ? fields.suggested : fields.additional;\n                } else {\n                    collection = fields.relational;\n                }\n            }\n\n            // Add field to found category\n            collection.push(field);\n\n            if (advanced) {\n                for (const subfield of field.fields) {\n                    sortSingleField(subfield, [...ancestors], collection, types);\n                }\n            }\n        };\n\n        // Sort fields in their respective categories\n        for (const field of this.fields) {\n            if (!field.isRelation) {\n                if (advanced) {\n                    sortSingleField(field, [], undefined, [\"all\"]);\n                } else {\n                    const acceptedTypes = res.header_types[index];\n                    sortSingleField(field, [], undefined, acceptedTypes);\n                }\n            }\n        }\n\n        return fields;\n    }\n\n    _updateComments(updatedColumn) {\n        for (const column of this.columns) {\n            column.comments = [];\n            column.errors = [];\n            column.resultNames = [];\n            column.importOptions =\n                column.fieldInfo && this.fieldsToHandle[column.fieldInfo.fieldPath];\n\n            if (!column.fieldInfo) {\n                continue;\n            }\n\n            // Fields of type \"char\", \"text\" or \"many2many\" can be specified multiple\n            // times and they will be concatenated, fields of other types must be unique.\n            if ([\"char\", \"text\", \"many2many\"].includes(column.fieldInfo.type)) {\n                if (column.fieldInfo.type === \"many2many\") {\n                    column.comments.push({\n                        type: \"info\",\n                        content: _t(\"To import multiple values, separate them by a comma.\"),\n                    });\n                }\n\n                // If multiple columns are mapped on the same field, inform\n                // the user that they will be concatenated.\n                const samefieldColumns = this.columns.filter(\n                    (col) => col.fieldInfo && col.fieldInfo.fieldPath === column.fieldInfo.fieldPath\n                );\n                if (samefieldColumns.length >= 2) {\n                    column.comments.push({\n                        type: \"info\",\n                        content: _t(\"This column will be concatenated in field\"),\n                        fieldName: column.fieldInfo.string,\n                    });\n                }\n            } else if (updatedColumn && column.id !== updatedColumn.id && updatedColumn.fieldInfo) {\n                // If column is mapped on an already mapped field, remove that field\n                // from the old column to keep it unique.\n                if (updatedColumn.fieldInfo.fieldPath === column.fieldInfo.fieldPath) {\n                    column.fieldInfo = null;\n                }\n            }\n        }\n    }\n\n    _getCSVFormattingOptions() {\n        return {\n            encoding: {\n                label: _t(\"Encoding:\"),\n                type: \"select\",\n                value: \"\",\n                options: [\n                    \"utf-8\",\n                    \"utf-16\",\n                    \"windows-1252\",\n                    \"latin1\",\n                    \"latin2\",\n                    \"big5\",\n                    \"gb18030\",\n                    \"shift_jis\",\n                    \"windows-1251\",\n                    \"koi8_r\",\n                ],\n            },\n            separator: {\n                label: _t(\"Separator:\"),\n                type: \"select\",\n                value: \"\",\n                options: [\n                    { value: \",\", label: _t(\"Comma\") },\n                    { value: \";\", label: _t(\"Semicolon\") },\n                    { value: \"\\t\", label: _t(\"Tab\") },\n                    { value: \" \", label: _t(\"Space\") },\n                ],\n            },\n            quoting: {\n                label: _t(\"Text Delimiter:\"),\n                type: \"input\",\n                value: '\"',\n            },\n            date_format: {\n                help: _t(\n                    \"Use YYYY to represent the year, MM for the month and DD for the day. Include separators such as a dot, forward slash or dash. You can use a custom format in addition to the suggestions provided. Leave empty to let Odoo guess the format (recommended)\"\n                ),\n                label: _t(\"Date Format:\"),\n                type: \"input\",\n                value: \"\",\n                options: [\n                    \"YYYY-MM-DD\",\n                    \"YYYY/MM/DD\",\n                    \"DD/MM/YYYY\",\n                    \"DDMMYYYY\",\n                    \"MM/DD/YYYY\",\n                    \"MMDDYYYY\",\n                ],\n            },\n            datetime_format: {\n                help: _t(\n                    \"Use HH for hours in a 24h system, use II in conjonction with 'p' for a 12h system. You can use a custom format in addition to the suggestions provided. Leave empty to let Odoo guess the format (recommended)\"\n                ),\n                label: _t(\"Datetime Format:\"),\n                type: \"input\",\n                value: \"\",\n                options: [\n                    \"YYYY-MM-DD HH:mm:SS\",\n                    \"YYYY/MM/DD HH:mm:SS\",\n                    \"DD/MM/YYYY HH:mm:SS\",\n                    \"DDMMYYYY HH:mm:SS\",\n                    \"MM/DD/YYYY II:mm:SS p\",\n                    \"MMDDYYYY II:mm:SS p\",\n                ],\n            },\n            float_thousand_separator: {\n                label: _t(\"Thousands Separator:\"),\n                type: \"select\",\n                value: \",\",\n                options: [\n                    { value: \",\", label: _t(\"Comma\") },\n                    { value: \".\", label: _t(\"Dot\") },\n                    { value: \"\", label: _t(\"No Separator\") },\n                ],\n            },\n            float_decimal_separator: {\n                label: _t(\"Decimals Separator:\"),\n                type: \"select\",\n                value: \".\",\n                options: [\n                    { value: \",\", label: _t(\"Comma\") },\n                    { value: \".\", label: _t(\"Dot\") },\n                ],\n            },\n        };\n    }\n}\n\n/**\n * @returns {BaseImportModel}columns\n */\nexport function useImportModel({ env, resModel, context, orm }) {\n    return useState(new BaseImportModel({ env, resModel, context, orm }));\n}\n", "/** @odoo-module **/\n\nimport { Component } from \"@odoo/owl\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { STATIC_ACTIONS_GROUP_NUMBER } from \"@web/search/action_menus/action_menus\";\n\nconst cogMenuRegistry = registry.category(\"cogMenu\");\n\n/**\n * 'Import records' menu\n *\n * This component is used to import the records for particular model.\n * @extends Component\n */\nexport class ImportRecords extends Component {\n    static template = \"base_import.ImportRecords\";\n    static components = { DropdownItem };\n    static props = {};\n\n    setup() {\n        this.action = useService(\"action\");\n    }\n\n    //---------------------------------------------------------------------\n    // Protected\n    //---------------------------------------------------------------------\n\n    importRecords() {\n        const { context, resModel } = this.env.searchModel;\n        this.action.doAction({\n            type: \"ir.actions.client\",\n            tag: \"import\",\n            params: { active_model: resModel, context },\n        });\n    }\n}\n\nexport const importRecordsItem = {\n    Component: ImportRecords,\n    groupNumber: STATIC_ACTIONS_GROUP_NUMBER,\n    isDisplayed: ({ config, isSmall }) =>\n        !isSmall &&\n        config.actionType === \"ir.actions.act_window\" &&\n        [\"kanban\", \"list\"].includes(config.viewType) &&\n        exprToBoolean(config.viewArch.getAttribute(\"import\"), true) &&\n        exprToBoolean(config.viewArch.getAttribute(\"create\"), true),\n};\n\ncogMenuRegistry.add(\"import-menu\", importRecordsItem, { sequence: 1 });\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { FileUploader } from \"@web/views/fields/file_handler\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { Component } from \"@odoo/owl\";\n\n\nexport class NewLoanComponent extends Component {\n    static template = \"account_accountant.NewLoan\";\n    static components = {\n        FileUploader,\n    };\n    static props = {\n        ...standardWidgetProps,\n        record: { type: Object, optional: true },\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n    }\n\n    async onFileUploaded(file) {\n        if (this.props.record && this.props.record.data.name){  //Save the record before calling the wizard\n            await this.props.record.model.root.save({reload: false});\n        }\n        const att_data = {\n            name: file.name,\n            mimetype: file.type,\n            datas: file.data,\n        };\n        const [att_id] = await this.orm.create(\"ir.attachment\", [att_data]);\n        const action = await this.orm.call(\"account.loan\", \"action_upload_amortization_schedule\", [this.props.record?.resId, att_id]);\n        this.action.doAction(action);\n    }\n\n    async openComputeWizard() {\n        if (this.props.record && this.props.record.data.name){  //Save the record before calling the wizard\n            await this.props.record.model.root.save({reload: false});\n        }\n        const action = await this.orm.call(\"account.loan\", \"action_open_compute_wizard\", [this.props.record?.resId]);\n        this.action.doAction(action);\n    }\n}\n\nexport const newLoan = {\n    component: NewLoanComponent,\n};\n\nregistry.category(\"view_widgets\").add(\"new_loan\", newLoan);\n", "import { useState, onWillStart } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { ImportAction } from \"@base_import/import_action/import_action\";\nimport { BaseImportModel } from \"@base_import/import_model\";\n\n\nclass AccountLoanImportModel extends BaseImportModel {\n    async init() {\n        return Promise.resolve();\n    }\n}\n\n\nexport class AccountLoanImportAction extends ImportAction {\n    setup() {\n        super.setup();\n\n        this.action = useService(\"action\");\n\n        this.model = useState(new AccountLoanImportModel({\n            env: this.env,\n            resModel: this.resModel,\n            context: this.props.action.params.context || {},\n            orm: this.orm,\n        }));\n\n        onWillStart(async () => {\n            if (this.props.action.params.context) {\n                this.model.id = this.props.action.params.context.wizard_id;\n                await super.handleFilesUpload([{ name: this.props.action.params.filename }])\n            }\n        });\n    }\n\n    async exit() {\n        if (this.model.resModel === \"account.loan.line\") {\n            const action = await this.orm.call(\"account.loan\", \"action_file_uploaded\", [this.model.context.default_loan_id]);\n            return this.action.doAction(action);\n        }\n        super.exit();\n    }\n}\n\nregistry.category(\"actions\").add(\"import_loan\", AccountLoanImportAction);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\n\nimport PurchaseAdditionalTourSteps from \"@purchase/js/tours/purchase_steps\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(PurchaseAdditionalTourSteps.prototype, {\n\n    _get_purchase_stock_steps: function () {\n        return [\n            {\n                trigger: \".o-form-buttonbox button[name='action_view_picking']\",\n            },\n        {\n            trigger: \".o-form-buttonbox button[name='action_view_picking']\",\n            content: _t(\"Receive the ordered products.\"),\n            tooltipPosition: \"bottom\",\n            run: 'click',\n        }, {\n            trigger: \".o_statusbar_buttons button[name='button_validate']\",\n            content: _t(\"Validate the receipt of all ordered products.\"),\n            tooltipPosition: \"bottom\",\n            run: 'click',\n        }, \n        {\n            trigger: \".modal-dialog\",\n        },\n        {\n            trigger: \".modal-footer .btn-primary\",\n            content: _t(\"Process all the receipt quantities.\"),\n            tooltipPosition: \"bottom\",\n            run: \"click\",\n        }, {\n            trigger: \".o_back_button a, .breadcrumb-item:not('.active'):last\",\n            content: _t('Go back to the purchase order to generate the vendor bill.'),\n            tooltipPosition: 'bottom',\n            run: \"click\",\n        }, {\n            trigger: \".o_statusbar_buttons button[name='action_create_invoice']\",\n            content: _t(\"Generate the draft vendor bill.\"),\n            tooltipPosition: \"bottom\",\n            run: 'click',\n        }\n        ];\n    }\n});\n\nexport default PurchaseAdditionalTourSteps;\n", "/** @odoo-module **/\n\nimport { BaseImportModel } from \"@base_import/import_model\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { _t } from \"@web/core/l10n/translation\";\n\npatch(BaseImportModel.prototype, {\n    async init() {\n        await super.init(...arguments);\n\n        if (this.resModel === \"account.bank.statement\") {\n            this.importTemplates.push({\n                label: _t(\"Import Template for Bank Statements\"),\n                template: \"/account_bank_statement_import/static/csv/account.bank.statement.csv\",\n            });\n        }\n    }\n});\n", "/** @odoo-module **/\nimport { patch } from \"@web/core/utils/patch\";\nimport { AccountFileUploader } from \"@account/components/account_file_uploader/account_file_uploader\";\nimport { BankRecFinishButtons } from \"@account_accountant/components/bank_reconciliation/finish_buttons\";\n\npatch(BankRecFinishButtons, {\n    components: {\n        AccountFileUploader,\n    }\n})\n", "/** @odoo-module **/\nimport { registry } from \"@web/core/registry\";\nimport { AccountFileUploader } from \"@account/components/account_file_uploader/account_file_uploader\";\nimport { UploadDropZone } from \"@account/components/upload_drop_zone/upload_drop_zone\";\nimport { BankRecKanbanView, BankRecKanbanController, BankRecKanbanRenderer } from \"@account_accountant/components/bank_reconciliation/kanban\";\nimport { useState } from \"@odoo/owl\";\n\nexport class BankRecKanbanUploadController extends BankRecKanbanController {\n    static components = {\n        ...BankRecKanbanController.components,\n        AccountFileUploader,\n    }\n}\n\nexport class BankRecUploadKanbanRenderer extends BankRecKanbanRenderer {\n    static template = \"account.BankRecKanbanUploadRenderer\";\n    static components = {\n        ...BankRecKanbanRenderer.components,\n        UploadDropZone,\n    };\n    setup() {\n        super.setup();\n        this.dropzoneState = useState({\n            visible: false,\n        });\n    }\n\n    onDragStart(ev) {\n        if (ev.dataTransfer.types.includes(\"Files\")) {\n            this.dropzoneState.visible = true\n        }\n    }\n}\n\nexport const BankRecKanbanUploadView = {\n    ...BankRecKanbanView,\n    Controller: BankRecKanbanUploadController,\n    Renderer: BankRecUploadKanbanRenderer,\n    buttonTemplate: \"account.BankRecKanbanButtons\",\n};\n\nregistry.category(\"views\").add('bank_rec_widget_kanban', BankRecKanbanUploadView, { force: true });\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { AccountFileUploader } from \"@account/components/account_file_uploader/account_file_uploader\";\nimport { UploadDropZone } from \"@account/components/upload_drop_zone/upload_drop_zone\";\nimport { bankRecListView, BankRecListController } from \"@account_accountant/components/bank_reconciliation/list\";\nimport { useState } from \"@odoo/owl\";\n\nexport class BankRecListUploadController extends BankRecListController {\n    static components = {\n        ...BankRecListController.components,\n        AccountFileUploader,\n    }\n}\n\nexport class BankRecListUploadRenderer extends ListRenderer {\n    static template = \"account.BankRecListUploadRenderer\";\n    static components = {\n        ...ListRenderer.components,\n        UploadDropZone,\n    }\n\n    setup() {\n        super.setup();\n        this.dropzoneState = useState({ visible: false });\n    }\n\n    onDragStart(ev) {\n        if (ev.dataTransfer.types.includes(\"Files\")) {\n            this.dropzoneState.visible = true\n        }\n    }\n}\n\nexport const bankRecListUploadView = {\n    ...bankRecListView,\n    Controller: BankRecListUploadController,\n    Renderer: BankRecListUploadRenderer,\n    buttonTemplate: \"account.BankRecListUploadButtons\",\n}\n\nregistry.category(\"views\").add(\"bank_rec_list\", bankRecListUploadView, { force: true });\n", "/** @odoo-module **/\n\nimport { Component, onWillDestroy, onWillStart, onWillUpdateProps, useState } from \"@odoo/owl\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\n\nconst CHECK_OCR_WAIT_DELAY = 5*1000;\n\nexport class StatusHeader extends Component {\n    static template = \"account_invoice_extract.Status\";\n    static props = standardFieldProps;\n\n    setup() {\n        this.state = useState({\n            status: this.props.record.data.extract_state,\n            errorMessage: this.props.record.data.extract_error_message,\n            retryLoading: false,\n            checkStatusLoading: false,\n        });\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n        this.busService = this.env.services.bus_service;\n\n        onWillStart(() => {\n            this.subscribeToChannel(this.props.record.data.extract_document_uuid);\n            this.busService.subscribe(\"state_change\", ({status, error_message})=> {\n                this.state.status = status;\n                this.state.errorMessage = error_message;\n            });\n            this.enableTimeout();\n        });\n\n        onWillDestroy(() => {\n            this.busService.deleteChannel(this.channelName);\n            clearTimeout(this.timeoutId);\n        });\n\n        onWillUpdateProps((nextProps) => {\n            if (nextProps.record.id !== this.props.record.id) {\n                this.state.errorMessage = nextProps.record.data.extract_error_message;\n                this.state.status = nextProps.record.data.extract_state;\n                this.subscribeToChannel(nextProps.record.data.extract_document_uuid);\n                this.enableTimeout();\n            }\n        });\n    }\n\n    subscribeToChannel(documentUUID) {\n        if (!documentUUID) {\n            return;\n        }\n        this.busService.deleteChannel(this.channelName);\n        this.channelName = `extract.mixin.status#${documentUUID}`;\n        this.busService.addChannel(this.channelName);\n    }\n\n    enableTimeout () {\n        if (!['waiting_extraction', 'extract_not_ready'].includes(this.state.status)) {\n            return;\n        }\n\n        clearTimeout(this.timeoutId);\n\n        this.timeoutId = setTimeout(async () => {\n            if (['waiting_extraction', 'extract_not_ready'].includes(this.state.status)) {\n                const [status, errorMessage] = (await this.orm.call(\n                    this.props.record.resModel,\n                    \"check_ocr_status\",\n                    [this.props.record.resId],\n                    {}\n                ))[0];\n                this.state.status = status;\n                this.state.errorMessage = errorMessage;\n            }\n        }, CHECK_OCR_WAIT_DELAY);\n    }\n\n    async checkOcrStatus() {\n        this.state.checkStatusLoading = true;\n        const [status, errorMessage] = (await this.orm.call(\n            this.props.record.resModel,\n            \"check_ocr_status\",\n            [this.props.record.resId],\n            {}\n        ))[0];\n        if (status === \"waiting_validation\") {\n            await this.refreshPage();\n            return;\n        }\n        this.state.status = status;\n        this.state.errorMessage = errorMessage;\n        this.state.checkStatusLoading = false;\n    }\n\n    async refreshPage() {\n        await this.action.switchView(\"form\", {\n            resId: this.props.record.resId,\n            resIds: this.props.record.resIds\n        });\n    }\n\n    async buyCredits() {\n        const actionData = await this.orm.call(this.props.record.resModel, \"buy_credits\", [this.props.record.resId], {});\n        this.action.doAction(actionData);\n    }\n\n    async retryDigitalization() {\n        this.state.retryLoading = true;\n        const [status, errorMessage, documentUUID] = await this.orm.call(this.props.record.resModel, \"action_manual_send_for_digitization\", [this.props.record.resId], {});\n        this.subscribeToChannel(documentUUID);\n        this.state.status = status;\n        this.state.errorMessage = errorMessage;\n        this.state.retryLoading = false;\n        this.enableTimeout();\n    }\n}\n\nregistry.category(\"fields\").add(\"extract_state_header\", {component: StatusHeader});\n", "/** @odoo-module **/\n\nimport { onWillStart } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ImportAction } from \"@base_import/import_action/import_action\";\nimport { useBankStatementCSVImportModel } from \"./bank_statement_csv_import_model\";\n\nexport class BankStatementImportAction extends ImportAction {\n    setup() {\n        super.setup();\n\n        this.action = useService(\"action\");\n\n        this.model = useBankStatementCSVImportModel({\n            env: this.env,\n            resModel: this.resModel,\n            context: this.props.action.params.context || {},\n            orm: this.orm,\n        });\n\n        this.env.config.setDisplayName(_t(\"Import Bank Statement\")); // Displayed in the breadcrumbs\n        this.state.filename = this.props.action.params.filename || undefined;\n\n        onWillStart(async () => {\n            if (this.props.action.params.context) {\n                this.model.id = this.props.action.params.context.wizard_id;\n                await super.handleFilesUpload([{ name: this.state.filename }])\n            }\n        });\n    }\n\n    async exit() {\n        if (this.model.statement_id) {\n            const res = await this.orm.call(\n                \"account.bank.statement\",\n                \"action_open_bank_reconcile_widget\",\n                [this.model.statement_id]\n            );\n            return this.action.doAction(res);\n        }\n        super.exit();\n    }\n}\n\nregistry.category(\"actions\").add(\"import_bank_stmt\", BankStatementImportAction);\n", "/** @odoo-module **/\n\nimport { useState } from \"@odoo/owl\";\nimport { BaseImportModel } from \"@base_import/import_model\";\n\nclass BankStatementCSVImportModel extends BaseImportModel {\n    async init() {\n        this.importOptionsValues.bank_stmt_import = {\n            value: true,\n        };\n        return Promise.resolve();\n    }\n\n    async _onLoadSuccess(res) {\n        super._onLoadSuccess(res);\n\n        if (!res.messages || res.messages.length === 0 || res.messages.length > 1) {\n            return;\n        }\n\n        const message = res.messages[0];\n        if (message.ids) {\n            this.statement_line_ids = message.ids\n        }\n\n        if (message.messages && message.messages.length > 0) {\n            this.statement_id = message.messages[0].statement_id\n        }\n    }\n}\n\n/**\n * @returns {BankStatementCSVImportModel}\n */\nexport function useBankStatementCSVImportModel({ env, resModel, context, orm }) {\n    return useState(new BankStatementCSVImportModel({ env, resModel, context, orm }));\n}\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { ImportAction } from \"@base_import/import_action/import_action\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useAccountMoveLineImportModel } from \"./account_import_model\";\n\nexport class AccountImportAction extends ImportAction {\n    setup() {\n        super.setup();\n        this.actionService = useService(\"action\");\n\n        this.model = useAccountMoveLineImportModel({\n            env: this.env,\n            resModel: this.resModel,\n            context: this.props.action.params.context || {},\n            orm: this.orm,\n        });\n    }\n\n    exit(resIds = null) {\n        if (resIds && [\"account.move.line\", \"account.account\", \"res.partner\"].includes(this.resModel)) {\n            const names = {\n                \"account.move.line\": _t(\"Journal Items\"),\n                \"account.account\": _t(\"Chart of Accounts\"),\n                \"res.partner\": _t(\"Customers\"),\n            }\n            const action = {\n                name: names[this.resModel],\n                res_model: this.resModel,\n                type: \"ir.actions.act_window\",\n                views: [[false, \"list\"], [false, \"form\"]],\n                view_mode: \"list\",\n                domain: [[\"id\", \"in\", resIds]],\n            }\n            if (this.resModel == \"account.move.line\") {\n                action.context = { \"search_default_posted\": 0 };\n            }\n            return this.actionService.doAction(action);\n        }\n        super.exit();\n    }\n};\n\nregistry.category(\"actions\").add(\"account_import_action\", AccountImportAction);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ControlPanel } from \"@web/search/control_panel/control_panel\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, onWillStart, onWillRender } from \"@odoo/owl\";\nimport { standardActionServiceProps } from \"@web/webclient/actions/action_service\";\n\nexport class AccountImportGuide extends Component {\n    static template = \"account_base_import.accountImportTemplate\";\n    static components = { ControlPanel };\n    static props = { ...standardActionServiceProps };\n    setup() {\n        this.actionService = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.env.config.setDisplayName(_t(\"Accounting Import Guide\"))\n        onWillStart(async () => {\n            const current_company_id = this.env.services.company.currentCompany.id\n            this.data = await this.orm.searchRead(\"res.company\", [[\"id\", \"=\", current_company_id]], [\"country_code\"])\n            this.isFecImportModuleInstalled = await this.orm.searchCount(\"ir.module.module\", [[\"name\", \"=\", \"l10n_fr_fec_import\"], [\"state\", \"=\", \"installed\"]]);\n        });\n        onWillRender(() => {\n            this.countryCode = this.data[0].country_code\n        });\n    }\n\n    _importAccountGuideAction(action) {\n        this.actionService.doAction(action);\n    }\n\n    _openModuleInstallation(module) {\n        this.actionService.doAction({\n            name: _t(\"Install a module\"),\n            res_model: \"ir.module.module\",\n            type: \"ir.actions.act_window\",\n            views: [[false, \"kanban\"], [false, \"list\"], [false, \"form\"]],\n            view_mode: \"kanban,list,form\",\n            context: {\n                \"search_default_name\": module,\n                \"search_default_extra\": true,\n            },\n        });\n    }\n};\n\nregistry.category(\"actions\").add(\"account_import_guide\", AccountImportGuide);\n", "/** @odoo-module **/\n\nimport { useState } from \"@odoo/owl\";\nimport { BaseImportModel } from \"@base_import/import_model\";\n\nclass AccountMoveLineImportModel extends BaseImportModel {\n    get importOptions() {\n        const options = super.importOptions;\n        if (this.resModel === \"account.move.line\") {\n            Object.assign(options.name_create_enabled_fields, {\n                journal_id: true,\n                account_id: true,\n                partner_id: true,\n            });\n        }\n        return options;\n    }\n}\n\n/**\n * @returns {AccountMoveLineImportModel}\n */\nexport function useAccountMoveLineImportModel({ env, resModel, context, orm }) {\n    return useState(new AccountMoveLineImportModel({ env, resModel, context, orm }));\n}\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { Component } from \"@odoo/owl\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nclass FollowupTrustPopOver extends Component {\n    static template = \"account_followup.FollowupTrustPopOver\";\n    static props = {\n        record: Object,\n        widget: Object,\n        close: { optional: true, type: Function },\n    };\n}\n\nclass FollowupTrustWidget extends Component {\n    static template = \"account_followup.FollowupTrustWidget\";\n    static props = {...standardFieldProps};\n    setup() {\n        super.setup();\n        const position = localization.direction === \"rtl\" ? \"bottom\" : \"right\";\n        this.popover = usePopover(FollowupTrustPopOver, { position });\n    }\n\n    displayTrust() {\n        var selections = this.props.record.fields.trust.selection;\n        var trust = this.props.record.data[this.props.name];\n        for (var i=0; i < selections.length; i++) {\n            if (selections[i][0] == trust) {\n                return selections[i][1];\n            }\n        }\n    }\n\n    onTrustClick(ev) {\n        this.popover.open(ev.currentTarget, {\n            record: this.props.record,\n            widget: this,\n        });\n    }\n\n    async setTrust(trust) {\n        this.props.record.update({ [this.props.name]: trust });\n        this.popover.close();\n    }\n}\n\nregistry.category(\"fields\").add(\"followup_trust_widget\", {\n    component: FollowupTrustWidget,\n});\n", "/** @odoo-module **/\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { registry } from \"@web/core/registry\";\n\nexport async function downloadAndCloseFollowupWizardAction(env, action) {\n    const url = action.params.url\n    try {\n        browser.open(url);\n    }\n    finally {\n        return { type: \"ir.actions.act_window_close\" };\n    }\n}\n\nregistry.category(\"actions\").add(\"close_followup_wizard\", downloadAndCloseFollowupWizardAction);\n", "/** @odoo-module **/\n\nimport { Component } from \"@odoo/owl\";\n\nexport class Box extends Component {\n    static template = \"account_invoice_extract.Box\";\n    static props = {\n        box: Object,\n        pageWidth: String,\n        pageHeight: String,\n        onClickBoxCallback: Function,\n    };\n    /**\n     * @override\n     */\n    setup() {\n        this.state = this.props.box;\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    get style() {\n        const style = [\n            `left: calc(${this.state.box_midX} * ${this.props.pageWidth})`,\n            `top: calc(${this.state.box_midY} * ${this.props.pageHeight})`,\n            `width: calc(${this.state.box_width} * ${this.props.pageWidth})`,\n            `height: calc(${this.state.box_height} * ${this.props.pageHeight})`,\n            `transform: translate(-50%, -50%) rotate(${this.state.box_angle}deg)`,\n            `-ms-transform: translate(-50%, -50%) rotate(${this.state.box_angle}deg)`,\n            `-webkit-transform: translate(-50%, -50%) rotate(${this.state.box_angle}deg)`,\n        ].join('; ');\n        return style;\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    onClick() {\n        this.props.onClickBoxCallback(this.state.id, this.state.page);\n    }\n};\n", "/** @odoo-module **/\n\nimport { Box } from '@account_invoice_extract/js/box';\nimport { Component } from \"@odoo/owl\";\n\nexport class BoxLayer extends Component {\n    static components = { Box };\n    static template = \"account_invoice_extract.BoxLayer\";\n    static props = {\n        boxes: Array,\n        pageLayer: {\n            validate: (pageLayer) => {\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 = pageLayer?.ownerDocument?.defaultView?.Element;\n                return (\n                    (Boolean(Element) &&\n                        (pageLayer instanceof Element || pageLayer instanceof window.Element)) ||\n                    (typeof pageLayer === \"object\" && pageLayer?.constructor?.name?.endsWith(\"Element\"))\n                );\n            },\n        },\n        onClickBoxCallback: Function,\n        mode: String,\n    };\n    /**\n     * @override\n     */\n    setup() {\n        this.state = {\n            boxes: this.props.boxes,\n        };\n\n        // Used to define the style of the contained boxes\n        if (this.isOnPDF) {\n            this.pageWidth = this.props.pageLayer.style.width;\n            this.pageHeight = this.props.pageLayer.style.height;\n        } else if (this.isOnImg) {\n            this.pageWidth = `${this.props.pageLayer.clientWidth}px`;\n            this.pageHeight = `${this.props.pageLayer.clientHeight}px`;\n        }\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    get style() {\n        if (this.isOnPDF) {\n            return 'width: ' + this.props.pageLayer.style.width + '; ' +\n                   'height: ' + this.props.pageLayer.style.height + ';';\n        } else if (this.isOnImg) {\n            return 'width: ' + this.props.pageLayer.clientWidth + 'px; ' +\n                   'height: ' + this.props.pageLayer.clientHeight + 'px; ' +\n                   'left: ' + this.props.pageLayer.offsetLeft + 'px; ' +\n                   'top: ' + this.props.pageLayer.offsetTop + 'px;';\n        }\n    }\n\n    get isOnImg() {\n        return this.props.mode === 'img';\n    }\n\n    get isOnPDF() {\n        return this.props.mode === 'pdf';\n    }\n};\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { getTemplate } from \"@web/core/templates\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { FormViewDialog } from \"@web/views/view_dialogs/form_view_dialog\";\n\nimport { AccountMoveFormRenderer } from '@account/components/account_move_form/account_move_form';\nimport { BoxLayer } from '@account_invoice_extract/js/box_layer';\n\nimport { App, onWillUnmount, reactive, useExternalListener, useState } from \"@odoo/owl\";\n\n/**\n * This is the renderer of the subview that adds OCR features on the attachment\n * preview. It displays boxes that have been generated by the OCR. The OCR\n * automatically selects a box, but the user can manually selects another box.\n */\nexport class InvoiceExtractFormRenderer extends AccountMoveFormRenderer {\n    /**\n     * @override\n     */\n    setup() {\n        super.setup();\n\n        /** @type {import(\"@mail/core/common/store_service\").Store} */\n        this.store = useState(useService(\"mail.store\"));\n        this.dialog = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n        this.mailPopoutService = useService(\"mail.popout\");\n\n        this._fieldsMapping = {\n            'partner_id': 'supplier',\n            'ref': 'invoice_id',\n            'invoice_date': 'date',\n            'invoice_date_due': 'due_date',\n            'currency_id': 'currency',\n            'quick_edit_total_amount': 'total',\n        };\n\n        // This contain the account.move record id of the fetched data.\n        // It needs to be tracked as, if another record is loaded, we should fetch the data of the new record.\n        this.dataMoveId = -1;\n\n        this.boxLayerApps = [];\n        this.activeField = undefined;\n        this.activeFieldEl = undefined;\n        this.boxes = [];\n        this.selectedBoxes = {};\n\n        this.state = useState({\n            visibleBoxes: {},\n        });\n\n        useExternalListener(window, \"focusin\", (event) => {\n            const field_widget = event.target.closest(\".o_field_widget\");\n            if (field_widget){\n                this.onFocusFieldWidget(field_widget);\n            }\n        });\n\n        useExternalListener(window, \"focusout\", (event) => {\n            if (event.target.closest(\".o_field_widget\") && !this.mailPopoutService.externalWindow){\n                this.onBlurFieldWidget();\n            }\n        });\n\n        onWillUnmount (() => {\n            this.destroyBoxLayers();\n        });\n    }\n\n    fetchBoxData() {\n        this.dataMoveId = this.props.record.resId;\n        return this.orm.call('account.move', 'get_boxes', [this.props.record.resId]);\n    }\n\n    /**\n     * Launch an Owl App with the box layer as root component.\n     */\n    createBoxLayerApp(props) {\n        props.onClickBoxCallback = this.onClickBox.bind(this);\n        return new App(BoxLayer, {\n            env: this.env,\n            dev: this.env.debug,\n            getTemplate,\n            props,\n            translatableAttributes: [\"data-tooltip\"],\n            translateFn: _t,\n        });\n    }\n\n    /**\n     * Renders the box layers on @element.\n     * If a box layer already exists, it is re-used.\n     */\n    renderBoxLayers(element) {\n        const proms = [];\n        // In case of img\n        if (element.classList.contains('img-fluid')) {\n            this.destroyBoxLayers();\n            const boxLayerApp = this.createBoxLayerApp({\n                boxes: this.state.visibleBoxes[0] || [],\n                mode: 'img',\n                pageLayer: element,\n            });\n            proms.push(boxLayerApp.mount(element.parentElement));\n            this.boxLayerApps = [boxLayerApp];\n        }\n        // In case of pdf\n        if (element.tagName === 'IFRAME') {\n            // Dynamically add css on the pdf viewer\n            const pdfDocument = element.contentDocument;\n            if (!pdfDocument.querySelector('head link#box_layer')) {\n                const win = this.mailPopoutService.externalWindow || window;\n                const boxLayerStylesheet = win.document.createElement('link');\n                boxLayerStylesheet.setAttribute('id', 'box_layer');\n                boxLayerStylesheet.setAttribute('rel', 'stylesheet');\n                boxLayerStylesheet.setAttribute('type', 'text/css');\n                boxLayerStylesheet.setAttribute('href', '/account_invoice_extract/static/src/css/box_layer.css');\n                pdfDocument.querySelector('head').append(boxLayerStylesheet);\n            }\n            const pageLayers = pdfDocument.querySelectorAll('.page');\n            for (const pageLayer of pageLayers) {\n                const pageNum = pageLayer.dataset['pageNumber'] - 1;\n                const boxLayerApp = this.createBoxLayerApp({\n                    boxes: this.state.visibleBoxes[pageNum] || [],\n                    mode: 'pdf',\n                    pageLayer: pageLayer,\n                });\n                proms.push(boxLayerApp.mount(pageLayer));\n                this.boxLayerApps.push(boxLayerApp);\n            }\n        }\n        return Promise.all(proms);\n    }\n\n    /**\n     * Renders the boxes on @attachment.\n     * It also determines which boxes should be visible according to the current active field.\n     */\n    renderInvoiceExtract(attachment) {\n        const thread = this.store.Thread.insert({\n            id: this.props.record.resId,\n            model: this.props.record.resModel,\n        });\n        const preview_attachment_id = thread.mainAttachment.id;\n        if (\n            ['in_invoice', 'in_refund', 'out_invoice', 'out_refund'].includes(this.props.record.data.move_type) &&\n            this.props.record.data.state === 'draft' &&\n            ['waiting_validation', 'validation_to_send'].includes(this.props.record.data.extract_state) &&\n            this.props.record.data.extract_attachment_id &&\n            preview_attachment_id === this.props.record.data.extract_attachment_id[0]\n        ) {\n            if (this.activeField !== undefined) {\n                if (this.dataMoveId !== this.props.record.resId) {\n                    for (const boxesForPage of Object.values(this.boxes)) {\n                        boxesForPage.length = 0;\n                    }\n                }\n                const dataToFetch = this.boxes.length === 0 || (this.dataMoveId !== this.props.record.resId);\n                const prom = dataToFetch ? this.fetchBoxData() : new Promise(resolve => resolve([]));\n                prom.then((boxes) => {\n                    boxes.map(b => reactive(b)).forEach((box) => {\n                        if (box.page in this.boxes) {\n                            this.boxes[box.page].push(box);\n                        }\n                        else {\n                            this.boxes[box.page] = [box];\n                        }\n                        if (box.user_selected) {\n                            this.selectedBoxes[box.feature] = box;\n                        }\n                    });\n                    for (const [page, boxesForPage] of Object.entries(this.boxes)) {\n                        if (page in this.state.visibleBoxes) {\n                            this.state.visibleBoxes[page].length = 0;\n                        }\n                        else {\n                            this.state.visibleBoxes[page] = [];\n                        }\n\n                        const visibleBoxesForPage = boxesForPage.filter((box) => {\n                            return (\n                                box.feature === this.activeField ||\n                                (box.feature === \"VAT_Number\" && this.activeField === \"supplier\")\n                            );\n                        });\n                        this.state.visibleBoxes[page].push(...visibleBoxesForPage);\n                    }\n                    this.renderBoxLayers(attachment)\n                });\n            }\n        }\n    }\n\n    /**\n     * Determines the DOM element on which the boxes must be rendered, then render them.\n     */\n    showBoxesForField(fieldName) {\n        // Case pdf (iframe)\n        const win = this.mailPopoutService.externalWindow || window;\n        const iframe = win.document.querySelector('.o-mail-Attachment iframe');\n        if (iframe) {\n            const iframeDoc = iframe.contentDocument;\n            if (iframeDoc) {\n                this.renderInvoiceExtract(iframe);\n                return;\n            }\n        }\n        // Case img\n        const attachment = win.document.getElementById('attachment_img');\n        if (attachment && attachment.complete) {\n            this.renderInvoiceExtract(attachment);\n            return;\n        }\n    }\n\n    resetActiveField() {\n        Object.values(this.state.visibleBoxes).forEach(boxesForPage => {\n            boxesForPage.length = 0;\n        });\n        this.activeField = undefined;\n        this.activeFieldEl = undefined;\n        this.destroyBoxLayers();\n    }\n\n    destroyBoxLayers() {\n        for (const boxLayerApp of this.boxLayerApps) {\n            boxLayerApp.destroy();\n        }\n        this.boxLayerApps = [];\n    }\n\n    async openCreatePartnerDialog(context) {\n        const ctx_from_db = await this.orm.call('account.move', 'get_partner_create_data', [[this.props.record.resId], context]);\n        this.dialog.add(\n            FormViewDialog,\n            {\n                resModel: 'res.partner',\n                context: Object.assign(\n                    ctx_from_db,\n                    Object.fromEntries(Object.entries(context).filter(([k, v]) => v !== undefined))\n                ),\n                title: _t(\"Create\"),\n                onRecordSaved: (record) => {\n                    this.props.record.update({ partner_id: [record.resId] });\n                },\n            }\n        );\n    }\n\n    /**\n     * Updates the field's value according to @newFieldValue.\n     */\n    async handleFieldChanged(fieldName, newFieldValue) {\n        let changes = {};\n        switch (fieldName) {\n            case 'date':\n                changes = { invoice_date: registry.category(\"parsers\").get(\"date\")(newFieldValue.split(' ')[0]) };\n                break;\n            case 'supplier':\n            case 'VAT_Number':\n                if (Array.isArray(newFieldValue) && newFieldValue.length == 2){\n                    changes = { partner_id: [newFieldValue[0]], partner_bank_id: [newFieldValue[1]] };\n                }\n                else if (Number.isFinite(newFieldValue) && newFieldValue !== 0) {\n                    changes = { partner_id: [newFieldValue] };\n                } else {\n                    await this.openCreatePartnerDialog({\n                        default_name: this.selectedBoxes['supplier']?.text,\n                        default_vat: this.selectedBoxes['VAT_Number']?.text\n                    });\n                    return;\n                }\n                break;\n            case 'due_date':\n                changes = { invoice_date_due: registry.category(\"parsers\").get(\"date\")(newFieldValue.split(' ')[0]) };\n                break;\n            case 'invoice_id':\n                changes =  ['out_invoice', 'out_refund'].includes(this.props.record.context.default_move_type) ? { name: newFieldValue } : { ref: newFieldValue };\n                break;\n            case 'currency':\n                changes = { currency_id: [newFieldValue] };\n                break;\n            case 'total':\n                changes = { quick_edit_total_amount: Number(newFieldValue) };\n                break;\n        }\n        this.props.record.update(changes)\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Called when a field widget gains focus.\n     * It serves as the entry point to render the boxes of the focused field.\n     */\n    onFocusFieldWidget(field_widget) {\n        const fieldName = this._fieldsMapping[field_widget.getAttribute('name')];\n\n        if (fieldName === undefined) {\n            this.resetActiveField();\n            return;\n        }\n\n        this.activeField = fieldName;\n        this.activeFieldEl = field_widget;\n\n        this.showBoxesForField(fieldName);\n    }\n\n    /**\n     * Called when a field widget loses focus.\n     * It hides all boxes.\n     */\n    onBlurFieldWidget() {\n        this.resetActiveField();\n    }\n\n    async onClickBox(boxId, boxPage) {\n        const box = this.boxes[boxPage].find(box => box.id === boxId);\n        const fieldName = box.feature;\n\n        // Unselect the previously selected box\n        if (this.selectedBoxes[fieldName]) {\n            this.selectedBoxes[fieldName].user_selected = false;\n        }\n\n        // Select the new box\n        box.user_selected = true;\n        this.selectedBoxes[fieldName] = box;\n\n        // Update the selected box in database\n        const newFieldValue = await this.orm.call(\n            'account.move',\n            'set_user_selected_box',\n            [[this.dataMoveId], boxId],\n        )\n\n        // Update the field's value\n        await this.handleFieldChanged(fieldName, newFieldValue);\n\n        if (['supplier', 'VAT_Number'].includes(box.feature)) {\n            this.activeFieldEl.querySelector('.o-autocomplete--dropdown-menu')?.classList.toggle('show');\n        } else if (['date', 'due_date'].includes(box.feature)) {\n            // For the date fields, we want to hide the calendar tooltip\n            // This is achieved by simulating an 'ESC' keypress\n            this.activeFieldEl.querySelector('input').dispatchEvent(new KeyboardEvent('keydown', {\n                key: 'Escape',\n            }));\n        }\n    }\n};\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { AccountMoveFormView } from '@account/components/account_move_form/account_move_form';\nimport { InvoiceExtractFormRenderer } from '@account_invoice_extract/js/invoice_extract_form_renderer';\n\nconst AccountMoveFormViewExtract = {\n    ...AccountMoveFormView,\n    Renderer: InvoiceExtractFormRenderer,\n};\n\nregistry.category(\"views\").add(\"account_move_form\", AccountMoveFormViewExtract, { force: true });\n", "import { formView } from \"@web/views/form/form_view\";\nimport { FormController } from \"@web/views/form/form_controller\";\nimport { registry } from \"@web/core/registry\";\nimport { useCheckDuplicateService } from \"./account_duplicate_transaction_hook\";\n\nexport class AccountDuplicateTransactionsFormController extends FormController {\n    setup() {\n        super.setup();\n        this.duplicateCheckService = useCheckDuplicateService();\n    }\n\n    async beforeExecuteActionButton(clickParams) {\n        if (clickParams.name === \"delete_selected_transactions\") {\n            const selected = this.duplicateCheckService.selectedLines;\n            if (selected.size) {\n                await this.orm.call(\n                    \"account.bank.statement.line\",\n                    \"unlink\",\n                    [Array.from(selected)],\n                );\n                this.env.services.action.doAction({type: 'ir.actions.client', tag: 'reload'});\n            }\n            return false;\n        }\n        return super.beforeExecuteActionButton(...arguments);\n    }\n\n    get cogMenuProps() {\n        const props = super.cogMenuProps;\n        props.items.action = [];\n        return props;\n    }\n}\n\nexport const form = { ...formView, Controller: AccountDuplicateTransactionsFormController };\n\nregistry.category(\"views\").add(\"account_duplicate_transactions_form\", form);\n", "import { useService } from \"@web/core/utils/hooks\";\nimport { useState } from \"@odoo/owl\";\n\nexport function useCheckDuplicateService() {\n    return useState(useService(\"account_online_synchronization.duplicate_check_service\"));\n}\n", "import { registry } from \"@web/core/registry\";\n\nclass AccountDuplicateTransactionsServiceModel {\n    constructor() {\n        this.selectedLines = new Set();\n    }\n\n    updateLIne(selected, id) {\n        this.selectedLines[selected ? \"add\" : \"delete\"](id);\n    }\n}\n\nconst duplicateCheckService = {\n    start(env, services) {\n        return new AccountDuplicateTransactionsServiceModel();\n    },\n};\n\nregistry\n    .category(\"services\")\n    .add(\"account_online_synchronization.duplicate_check_service\", duplicateCheckService);\n", "import { onMounted } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { X2ManyField, x2ManyField } from \"@web/views/fields/x2many/x2many_field\";\nimport { useCheckDuplicateService } from \"./account_duplicate_transaction_hook\";\n\nexport class AccountDuplicateTransactionsListRenderer extends ListRenderer {\n    static template = \"account_online_synchronization.AccountDuplicateTransactionsListRenderer\";\n    static recordRowTemplate = \"account_online_synchronization.AccountDuplicateTransactionsRecordRow\";\n\n    setup() {\n        super.setup();\n        this.duplicateCheckService = useCheckDuplicateService();\n\n        onMounted(() => {\n            this.deleteButton = document.querySelector('button[name=\"delete_selected_transactions\"]');\n            this.deleteButton.disabled = true;\n        });\n    }\n\n    toggleRecordSelection(selected, record) {\n        this.duplicateCheckService.updateLIne(selected, record.data.id);\n        this.deleteButton.disabled = this.duplicateCheckService.selectedLines.size === 0;\n    }\n\n    get hasSelectors() {\n        return true;\n    }\n\n    getRowClass(record) {\n        let classes = super.getRowClass(record);\n        const firstIdsInGroup = this.env.model.root.data.first_ids_in_group;\n        if (firstIdsInGroup instanceof Array && firstIdsInGroup.includes(record.data.id)) {\n            classes += \" account_duplicate_transactions_lines_list_x2many_group_line\";\n        }\n        return classes;\n    }\n}\n\nexport class AccountDuplicateTransactionsLinesListX2ManyField extends X2ManyField {\n    static components = {\n        ...X2ManyField.components,\n        ListRenderer: AccountDuplicateTransactionsListRenderer,\n    };\n}\n\nregistry.category(\"fields\").add(\"account_duplicate_transactions_lines_list_x2many\", {\n    ...x2ManyField,\n    component: AccountDuplicateTransactionsLinesListX2ManyField,\n});\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { useService, useBus } from \"@web/core/utils/hooks\";\nimport { SIZES } from \"@web/core/ui/ui_service\";\nimport { Component, useState, useRef, onWillStart } from \"@odoo/owl\";\n\nclass BankConfigureWidget extends Component {\n    static template = \"account.BankConfigureWidget\";\n    static props = {\n        ...standardWidgetProps,\n    }\n    setup() {\n        this.container = useRef(\"container\");\n        this.allInstitutions = [];\n        this.state = useState({\n            isLoading: true,\n            institutions: [],\n            gridStyle: \"grid-template-columns: repeat(5, minmax(90px, 1fr));\"\n        });\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n        this.ui = useService(\"ui\");\n        onWillStart(this.fetchInstitutions);\n        useBus(this.ui.bus, \"resize\", this.computeGrid);\n    }\n\n    computeGrid() {\n        if (this.allInstitutions.length > 4) {\n            let containerWidth = this.container.el ? this.container.el.offsetWidth - 32 : 0;\n            // when the container width can't be computed, use the screen size and number of journals.\n            if (!containerWidth) {\n                if (this.ui.size >= SIZES.XXL) {\n                    containerWidth = window.innerWidth / (this.props.record.model.root.count < 6 ? 2 : 3);\n                } else {\n                    containerWidth = Math.max(this.ui.size * 100, 400);\n                }\n            }\n            const canFit = Math.floor(containerWidth / 100);\n            const numberOfRows = (Math.floor((this.allInstitutions.length + 1) / 2) >= canFit) + 1;\n            this.state.gridStyle = `grid-template-columns: repeat(${canFit}, minmax(90px, 1fr));\n                                    grid-template-rows: repeat(${numberOfRows}, 1fr);\n                                    grid-auto-rows: 0px;\n                                   `;\n        }\n        this.state.institutions = this.allInstitutions;\n    }\n\n    async fetchInstitutions() {\n        this.orm.silent.call(this.props.record.resModel, \"fetch_online_sync_favorite_institutions\", [this.props.record.resId])\n        .then((response) => {\n            this.allInstitutions = response;\n        })\n        .finally(() => {\n            this.state.isLoading = false;\n            this.computeGrid();\n        });\n    }\n\n    async connectBank(institutionId=null) {\n        const action = await this.orm.call(\"account.online.link\", \"action_new_synchronization\", [[]], {\n            preferred_inst: institutionId,\n            journal_id: this.props.record.resId,\n        })\n        this.action.doAction(action);\n    }\n\n    async fallbackConnectBank() {\n        const action = await this.orm.call('account.online.link', 'create_new_bank_account_action', [], {\n            context: {\n                active_model: 'account.journal',\n                active_id: this.props.record.resId,\n            }\n        });\n        this.action.doAction(action);\n    }\n}\n\nexport const bankConfigureWidget = {\n    component: BankConfigureWidget,\n}\n\nregistry.category(\"view_widgets\").add(\"bank_configure\", bankConfigureWidget);\n", "/** @odoo-module **/\n\nimport { Component } from \"@odoo/owl\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nconst cogMenuRegistry = registry.category(\"cogMenu\");\n\n/**\n * 'Fetch Missing Transactions' menu\n *\n * This component is used to open a wizard allowing the user to fetch their missing/pending\n * transaction since a specific date.\n * It's only available in the bank reconciliation widget.\n * By default, if there is only one selected journal, this journal is directly selected.\n * In case there is no selected journal or more than one, we let the user choose which\n * journal he/she wants. This part is handled by the server.\n * @extends Component\n */\nexport class FetchMissingTransactions extends Component {\n    static template = \"account_online_synchronization.FetchMissingTransactions\";\n    static components = { DropdownItem };\n    static props = {};\n\n    setup() {\n        this.action = useService(\"action\");\n    }\n\n    //---------------------------------------------------------------------\n    // Protected\n    //---------------------------------------------------------------------\n\n    async openFetchMissingTransactionsWizard() {\n        const { context } = this.env.searchModel;\n        const activeModel = context.active_model;\n        let activeIds = [];\n        if (activeModel === \"account.journal\") {\n            activeIds = context.active_ids;\n        } else if (!!context.default_journal_id) {\n            activeIds = context.default_journal_id;\n        }\n        // We have to use this.env.services.orm.call instead of using useService\n        // for a specific reason. useService implies that function calls with\n        // are \"protected\", it means that if the component is closed the\n        // response will be pending and the code stop their execution.\n        // By passing directly from the env, this protection is not activated.\n        const action = await this.env.services.orm.call(\n            \"account.journal\",\n            \"action_open_missing_transaction_wizard\",\n            [activeIds]\n        );\n        return this.action.doAction(action);\n    }\n}\n\nexport const fetchMissingTransactionItem = {\n    Component: FetchMissingTransactions,\n    groupNumber: 5,\n    isDisplayed: ({ config, isSmall }) => {\n        return !isSmall &&\n        config.actionType === \"ir.actions.act_window\" &&\n        [\"kanban\", \"list\"].includes(config.viewType) &&\n        [\"bank_rec_widget_kanban\", \"bank_rec_list\"].includes(config.viewSubType);\n    },\n};\n\ncogMenuRegistry.add(\"fetch-missing-transaction-menu\", fetchMissingTransactionItem, { sequence: 1 });\n", "import { Component } from \"@odoo/owl\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * 'Find Duplicate Transactions' menu\n *\n * This component is used to open a wizard allowing the user to find duplicate\n * transactions since a specific date.\n * It's only available in the bank reconciliation widget.\n * By default, if there is only one selected journal, this journal is directly selected.\n * In case there is no selected journal or more than one, we let the user choose.\n * @extends Component\n */\nexport class FindDuplicateTransactions extends Component {\n    static template = \"account_online_synchronization.FindDuplicateTransactions\";\n    static components = { DropdownItem };\n    static props = {};\n\n    setup() {\n        this.action = useService(\"action\");\n    }\n\n    //---------------------------------------------------------------------\n    // Protected\n    //---------------------------------------------------------------------\n\n    async openFindDuplicateTransactionsWizard() {\n        const { context } = this.env.searchModel;\n        const activeModel = context.active_model;\n        let activeIds = [];\n        if (activeModel === \"account.journal\") {\n            activeIds = context.active_ids;\n        } else if (context.default_journal_id) {\n            activeIds = context.default_journal_id;\n        }\n        return this.action.doActionButton({\n            type: \"object\",\n            resModel: \"account.journal\",\n            name:\"action_open_duplicate_transaction_wizard\",\n            resIds: activeIds,\n        })\n    }\n}\n\nexport const findDuplicateTransactionItem = {\n    Component: FindDuplicateTransactions,\n    groupNumber: 5, // same group as fetch missing transactions\n    isDisplayed: ({ config, isSmall }) => {\n        return (\n            !isSmall &&\n            config.actionType === \"ir.actions.act_window\" &&\n            [\"kanban\", \"list\"].includes(config.viewType) &&\n            [\"bank_rec_widget_kanban\", \"bank_rec_list\"].includes(config.viewSubType)\n        )\n    },\n};\n\nregistry.category(\"cogMenu\").add(\n    \"find-duplicate-transaction-menu\",\n    findDuplicateTransactionItem,\n    { sequence: 3 }, // after fetch missing transactions\n);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, useState } from \"@odoo/owl\";\n\nclass ConnectedUntil extends Component {\n    static template = \"account_online_synchronization.ConnectedUntil\";\n    static props = { ...standardWidgetProps };\n\n    setup() {\n        this.state = useState({\n            isHovered: false,\n            displayReconnectButton: false,\n        });\n\n        if (this.isConnectionExpiredIn(0)) {\n            this.state.displayReconnectButton = true;\n        }\n\n        this.action = useService(\"action\");\n        this.orm = useService(\"orm\");\n    }\n\n    get cssClasses() {\n        let cssClasses = \"text-nowrap w-100\";\n        if (this.isConnectionExpiredIn(7)) {\n            cssClasses += this.isConnectionExpiredIn(3) ? \" text-danger\" : \" text-warning\";\n        }\n        return cssClasses;\n    }\n\n    onMouseEnter() {\n        this.state.isHovered = true;\n    }\n\n    onMouseLeave() {\n        this.state.isHovered = false;\n    }\n\n    isConnectionExpiredIn(nbDays) {\n        return this.props.record.data.expiring_synchronization_due_day <= nbDays;\n    }\n\n    async extendConnection() {\n        const action = await this.orm.call(\n            \"account.journal\",\n            \"action_extend_consent\",\n            [this.props.record.resId],\n            {}\n        );\n        this.action.doAction(action);\n    }\n}\n\nexport const connectedUntil = {\n    component: ConnectedUntil,\n};\n\nregistry.category(\"view_widgets\").add(\"connected_until_widget\", connectedUntil);\n", "/** @odoo-module **/\n\nimport { onMounted, useState } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\nimport { RadioField, radioField } from \"@web/views/fields/radio/radio_field\";\nimport { useService } from '@web/core/utils/hooks';\n\n\nclass OnlineAccountRadio extends RadioField {\n    static template = \"account_online_synchronization.OnlineAccountRadio\";\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.state = useState({balances: {}});\n\n        onMounted(async () => {\n            this.state.balances = await this.loadData();\n            // Make sure the first option is selected by default.\n            this.onChange(this.items[0]);\n        });\n    }\n\n    async loadData() {\n        const ids = this.items.map(i => i[0]);\n        return await this.orm.call(\"account.online.account\", \"get_formatted_balances\", [ids]);\n    }\n\n    getBalanceName(itemID) {\n        return this.state.balances?.[itemID]?.[0] ?? \"Loading ...\";\n    }\n\n    isNegativeAmount(itemID) {\n        // In case of the value is undefined, it will return false as intended.\n        return this.state.balances?.[itemID]?.[1] < 0;\n    }\n}\n\nregistry.category(\"fields\").add(\"online_account_radio\", {\n    ...radioField,\n    component: OnlineAccountRadio,\n});\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, useState, onWillStart } from \"@odoo/owl\";\n\nclass RefreshSpin extends Component {\n    static template = \"account_online_synchronization.RefreshSpin\";\n    static props = { ...standardWidgetProps };\n\n    setup() {\n        this.state = useState({\n            isHovered: false,\n            fetchingStatus: false,\n            connectionStateDetails: null,\n        });\n\n        this.actionService = useService(\"action\");\n        this.busService = this.env.services.bus_service;\n        this.orm = useService(\"orm\");\n        this.state.fetchingStatus = this.props.record.data.online_sync_fetching_status;\n\n        this.busService.subscribe(\"online_sync\", (notification) => {\n            if (notification?.id === this.recordId && notification?.connection_state_details) {\n                this.state.connectionStateDetails = notification.connection_state_details;\n            }\n        });\n\n        onWillStart(() => {\n            this._initConnectionStateDetails();\n        });\n    }\n\n    refresh() {\n        this.actionService.restore(this.actionService.currentController.jsId);\n    }\n\n    onMouseEnter() {\n        this.state.isHovered = true;\n    }\n\n    onMouseLeave() {\n        this.state.isHovered = false;\n    }\n\n    async openAction() {\n        /**\n         * This function is used to open the action that the asynchronous process saved\n         * on the databsase. It allows users to call the action when they want and not when\n         * the process is over.\n         */\n        const action = await this.orm.call(\n            \"account.journal\",\n            \"action_open_dashboard_asynchronous_action\",\n            [this.recordId],\n        );\n        this.actionService.doAction(action);\n        this.state.connectionStateDetails = null;\n    }\n\n    async fetchTransactions() {\n        /**\n         * This function call the function to fetch transactions.\n         * In the main case, we don't do anything after calling the function.\n         * The idea is that websockets will update the status by themselves.\n         * In one specific case, we have to return an action to the user to open\n         * the Odoo Fin iframe to refresh the connection.\n         */\n        const action = await this.orm.call(\"account.journal\", \"manual_sync\", [this.recordId]);\n        this.state.connectionStateDetails = { status: \"fetching\" };\n        if (action) {\n            this.actionService.doAction(action);\n        }\n    }\n\n    _initConnectionStateDetails() {\n        /**\n         * This function is used to get the last state of the connection (if there is one)\n         */\n        const kanbanDashboardData = JSON.parse(this.props.record.data.kanban_dashboard);\n        this.state.connectionStateDetails = kanbanDashboardData?.connection_state_details;\n    }\n\n    get recordId() {\n        return this.props.record.data.id;\n    }\n\n    get connectionStatus() {\n        return this.state.connectionStateDetails?.status;\n    }\n}\n\nexport const refreshSpin = {\n    component: RefreshSpin,\n};\n\nregistry.category(\"view_widgets\").add(\"refresh_spin_widget\", refreshSpin);\n", "/** @odoo-module **/\n\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { ListController } from \"@web/views/list/list_controller\";\nimport { registry } from \"@web/core/registry\";\nimport { listView } from \"@web/views/list/list_view\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class TransientBankStatementLineListController extends ListController {\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n    }\n\n    async onClickImportTransactions() {\n        const resIds = await this.getSelectedResIds();\n        const resultAction = await this.orm.call(\"account.bank.statement.line.transient\", \"action_import_transactions\", [resIds]);\n        this.action.doAction(resultAction);\n    }\n}\n\nexport class TransientBankStatementLineListRenderer extends ListRenderer {\n\n    static template = \"account_online_synchronization.TransientBankStatementLineRenderer\";\n\n    setup() {\n        super.setup();\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n    }\n\n    async openManualEntries() {\n        if (this.env.searchModel.context.active_model === \"account.missing.transaction.wizard\" && this.env.searchModel.context.active_ids) {\n            const activeIds = this.env.searchModel.context.active_ids;\n            const action = await this.orm.call(\"account.missing.transaction.wizard\", \"action_open_manual_bank_statement_lines\", activeIds);\n            this.action.doAction(action);\n        }\n    }\n\n}\n\nexport const TransientBankStatementLineListView = {\n    ...listView,\n    Renderer: TransientBankStatementLineListRenderer,\n    Controller: TransientBankStatementLineListController,\n    buttonTemplate: \"TransientBankStatementLineButtonTemplate\",\n}\n\nregistry.category(\"views\").add(\"transient_bank_statement_line_list_view\", TransientBankStatementLineListView);\n", "import { patch } from \"@web/core/utils/patch\";\nimport { BankRecKanbanController } from \"@account_accountant/components/bank_reconciliation/kanban\";\n\npatch(BankRecKanbanController.prototype, {\n    setup() {\n        super.setup();\n        this.displayDuplicateWarning = !!this.props.context.duplicates_from_date;\n    },\n    async onWarningClick () {\n        const { context } = this.env.searchModel;\n        return this.action.doActionButton({\n            type: \"object\",\n            resModel: \"account.journal\",\n            name:\"action_open_duplicate_transaction_wizard\",\n            resId: this.state.journalId,\n            args: JSON.stringify([context.duplicates_from_date]),\n        })\n    },\n})\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { loadJS } from \"@web/core/assets\";\nimport { cookie } from \"@web/core/browser/cookie\";\nimport { markup } from \"@odoo/owl\";\nconst actionRegistry = registry.category('actions');\n/* global OdooFin */\n\nfunction OdooFinConnector(parent, action) {\n    const orm = parent.services.orm;\n    const actionService = parent.services.action;\n    const notificationService = parent.services.notification;\n    const debugMode = parent.debug;\n\n    const id = action.id;\n    action.params.colorScheme = cookie.get(\"color_scheme\");\n    let mode = action.params.mode || 'link';\n    // Ensure that the proxyMode is valid\n    const modeRegexp = /^[a-z0-9-_]+$/;\n    const runbotRegexp = /^https:\\/\\/[a-z0-9-_]+\\.[a-z0-9-_]+\\.odoo\\.com$/;\n    if (!modeRegexp.test(action.params.proxyMode) && !runbotRegexp.test(action.params.proxyMode)) {\n        return;\n    }\n    let url = 'https://' + action.params.proxyMode + '.odoofin.com/proxy/v1/odoofin_link';\n    if (runbotRegexp.test(action.params.proxyMode)) {\n        url = action.params.proxyMode + '/proxy/v1/odoofin_link';\n    }\n    let actionResult = false;\n\n    loadJS(url)\n        .then(function () {\n            // Create and open the iframe\n            const params = {\n                data: action.params,\n                proxyMode: action.params.proxyMode,\n                onEvent: async function (event, data) {\n                    switch (event) {\n                        case 'close':\n                            return;\n                        case 'reload':\n                            return actionService.doAction({type: 'ir.actions.client', tag: 'reload'});\n                        case 'notification':\n                            notificationService.add(data.message, data);\n                            break;\n                        case 'exchange_token':\n                            await orm.call('account.online.link', 'exchange_token',\n                                [[id], data], {context: action.context});\n                            break;\n                        case 'success':\n                            mode = data.mode || mode;\n                            actionResult = await orm.call('account.online.link', 'success', [[id], mode, data], {context: action.context});\n                            actionResult.help = markup(actionResult.help)\n                            return actionService.doAction(actionResult);\n                        case 'connect_existing_account':\n                            actionResult = await orm.call('account.online.link', 'connect_existing_account', [data], {context: action.context});\n                            actionResult.help = markup(actionResult.help)\n                            return actionService.doAction(actionResult);\n                        default:\n                            return;\n                    }\n                },\n                onAddBank: async function () {\n                    // If the user doesn't find his bank\n                    actionResult = await orm.call('account.online.link', 'create_new_bank_account_action',\n                    [], {context: action.context});\n                    actionResult.help = markup(actionResult.help)\n                    return actionService.doAction(actionResult);\n                }\n            };\n            // propagate parent debug mode to iframe\n            if (typeof debugMode !== \"undefined\" && debugMode) {\n                params.data[\"debug\"] = debugMode;\n            }\n            OdooFin.create(params);\n            OdooFin.open();\n        });\n    return;\n}\n\nactionRegistry.add('odoo_fin_connector', OdooFinConnector);\n\nexport default OdooFinConnector;\n", "/* @odoo-module */\n\nimport { ActivityMenu } from \"@mail/core/web/activity_menu\";\nimport { patch } from \"@web/core/utils/patch\";\n\nimport \"@crm/activity_menu_patch\";\n\npatch(ActivityMenu.prototype, {\n    availableViews(group) {\n        if (group.model === \"crm.lead\") {\n            return [\n                [false, \"list\"],\n                [false, \"kanban\"],\n                [false, \"form\"],\n                [false, \"calendar\"],\n                [false, \"pivot\"],\n                [false, \"cohort\"],\n                [false, \"map\"],\n                [false, \"activity\"],\n            ];\n        }\n        return super.availableViews(...arguments);\n    },\n});\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport \"@crm/js/tours/crm\";\nimport { patch } from \"@web/core/utils/patch\";\n\nimport { markup } from \"@odoo/owl\";\n\npatch(registry.category(\"web_tour.tours\").get(\"crm_tour\"), {\n    steps() {\n        const originalSteps = super.steps();\n        const DragOppToWonStepIndex = originalSteps.findIndex(\n            (step) => step.id === \"drag_opportunity_to_won_step\"\n        );\n        originalSteps.splice(\n            DragOppToWonStepIndex + 1,\n            0,\n            {\n                /**\n                 * Add some steps between \"Drag your opportunity to <b>Won</b> when you get\n                 * the deal. Congrats!\" and \"Let\u2019s have a look at an Opportunity.\" to\n                 * include the steps related to the lead generation (crm_iap_mine).\n                 * This eases the on boarding for the Lead Generation process.\n                 *\n                 */\n                trigger: \".o_button_generate_leads\",\n                content: markup(_t(\"Looking for more opportunities?<br>Try the <b>Lead Generation</b> tool.\")),\n                tooltipPosition: \"bottom\",\n                run: \"click .o_button_generate_leads\",\n            },\n            {\n                trigger: \".modal-body .o_industry\",\n                content: _t(\"Which Industry do you want to target?\"),\n                tooltipPosition: \"right\",\n                run: \"click\",\n            },\n            {\n                isActive: [\"manual\"],\n                trigger: \".modal-footer button[name=action_submit]\",\n                content: _t(\"Now, just let the magic happen!\"),\n                tooltipPosition: \"bottom\",\n                run: \"click\",\n            },\n            {\n                isActive: [\"auto\"],\n                trigger: \".modal-footer button[special=cancel]\",\n                content: _t(\"Now, just let the magic happen!\"),\n                tooltipPosition: \"bottom\",\n                run: \"click\",\n            }\n        );\n        return originalSteps;\n    },\n});\n", "/** @odoo-module */\n\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useChildRef } from \"@web/core/utils/hooks\";\nimport { Component } from \"@odoo/owl\";\n\n//  extending the dialog component to add the export dialog\nexport class ExportDialog extends Component {\n    setup() {\n        this.env.dialogData.dismiss = () => this._cancel();\n        this.modalRef = useChildRef();\n        this.isProcess = false;\n    }\n    async _cancel() {\n        return this.execButton(this.props.cancel);\n    }\n    async _confirm() {\n        return this.execButton(this.props.confirm);\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    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//  setting properties of ExportDialog\nExportDialog.template = \"ExportPdf.List\";\nExportDialog.components = { Dialog };\nExportDialog.props = {\n    close: Function,\n    title: {\n        validate: (m) => {\n            return (\n                typeof m === \"string\" || (typeof m === \"object\" && typeof m.toString === \"function\")\n            );\n        },\n        optional: true,\n    },\n    body: String,\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};\nExportDialog.defaultProps = {\n    confirmLabel: _t(\"Export\"),\n    cancelLabel: _t(\"Cancel\"),\n    confirmClass: \"btn-primary\",\n    title: _t(\"Export PDF\"),\n};\n", "/** @odoo-module **/\n\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { registry } from \"@web/core/registry\";\n//import { ListController } from '@web/views/list/list_controller';\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { STATIC_ACTIONS_GROUP_NUMBER } from \"@web/search/action_menus/action_menus\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { ExportDialog } from \"./export_dialog\";\nimport { Component } from \"@odoo/owl\";\nimport { user } from \"@web/core/user\";\nconst cogMenuRegistry = registry.category(\"cogMenu\");\n/**\n * 'Export PDF' menu\n *\n * This component is used to export PDF the records for particular model.\n * @extends Component\n */\nexport class ExportPdf extends Component {\n    static template = \"web.ExportPdf\";\n    static components = { DropdownItem };\n    //---------------------------------------------------------------------\n    // Protected\n    //---------------------------------------------------------------------\n//  method to define the functionality when clicking on the export pdf menu\n    async onDirectExportPdf() {\n        this.env.searchModel.trigger('direct-export-pdf');\n        const exportField = [];\n        const exportedFields = Object.values(this.env.model.config.fields).map(field => ({\n            name: field.name,  // Assuming `field` has a `name` property\n            label: field.label || field.string,  // Use field properties like label or string\n        }));\n        const resIds = await this.env.model.root.getResIds();\n        this.env.model.dialog.add(ExportDialog, {\n                title: _t(\"Export PDF\"),\n                context: exportedFields,\n                confirm: async () => {\n                    let checkboxes = document.querySelectorAll(`#${'check'} input[type=\"checkbox\"]`);\n                    checkboxes.forEach(item => {\n                        if (item.checked === true){\n                            exportField.push({name:item.name,label:item.value})\n                        }\n                    });\n                    var length_field = Array.from(Array(exportField.length).keys());\n                    await rpc('/get_data',{\n                        'model':this.env.model.config.resModel,\n                        'res_ids':resIds.length > 0 && resIds,\n                        'fields':exportField,\n                        'grouped_by':this.env.model.config.groupBy,\n                        'context': this.env.model.config.context,\n                        'domain':this.env.model.config.domain,\n                    }).then(data => {\n                        if (this.env.model.config.groupBy[0]){\n                            var action = {\n                                'type': 'ir.actions.report',\n                                'report_type': 'qweb-pdf',\n                                'report_name':'export_view_pdf.export_in_pdf_group_by',\n                                'data':{'length':length_field,'group_len':[0,1,2,3],'record':data,}\n                            };\n                        }\n                        else{\n                            var action = {\n                                'type': 'ir.actions.report',\n                                'report_type': 'qweb-pdf',\n                                'report_name':'export_view_pdf.export_in_pdf',\n                                'data':{'length':length_field,'record':data}\n                            };\n                        }\n                        return this.env.model.action.doAction(action);\n                    });\n                },\n                cancel: () => {},\n            });\n    }\n}\n// for adding export pdf menu item\nexport const exportPdfItem = {\n    Component: ExportPdf,\n    groupNumber: STATIC_ACTIONS_GROUP_NUMBER,\n    isDisplayed: async (env) =>\n        env.config.viewType === \"list\" &&\n        !env.model.root.selection.length &&\n        await user.hasGroup(\"base.group_allow_export\") &&\n        exprToBoolean(env.config.viewArch.getAttribute(\"export_xlsx\"), true),\n};\ncogMenuRegistry.add(\"export-pdf-menu\", exportPdfItem, { sequence: 10 });\n", "import { AttendeeCalendarModel } from \"@calendar/views/attendee_calendar/attendee_calendar_model\";\nimport { serializeDate } from \"@web/core/l10n/dates\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(AttendeeCalendarModel.prototype, {\n    setup() {\n        super.setup(...arguments)\n        this.data.workingHours = {};\n    },\n\n    get workingHours() {\n        return this.data.workingHours;\n    },\n\n    async updateData(data) {\n        await super.updateData(...arguments)\n        data.workingHours = await this.fetchWorkingHours(data);\n    },\n\n    async fetchWorkingHours(data){\n        if (this.meta.scale !== \"day\" && this.meta.scale !== \"week\"){\n            return [];\n        }\n        const attendeeFilters = data.filterSections.partner_ids;\n        const activeAttendeeIds = attendeeFilters.filters\n            .filter((filter) => filter.type !== \"all\" && filter.value && filter.active)\n            .map((filter) => filter.value);\n        const allFilter = attendeeFilters.filters.find((filter) => filter.type === \"all\");\n        return this.orm.call(\"res.partner\", \"get_working_hours_for_all_attendees\", [\n            activeAttendeeIds,\n            serializeDate(data.range.start),\n            serializeDate(data.range.end),\n            allFilter?.active\n        ]);\n    },\n});\n", "import { AttendeeCalendarCommonRenderer } from \"@calendar/views/attendee_calendar/common/attendee_calendar_common_renderer\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { onWillUpdateProps } from \"@odoo/owl\";\n\npatch(AttendeeCalendarCommonRenderer.prototype, {\n\tsetup() {\n\t\tsuper.setup(...arguments);\n\t\tonWillUpdateProps(() => {\n\t\t\tthis.fc.api.setOption(\"businessHours\", this.props.model.workingHours)\n\t\t});\n\t},\n\tget options() {\n\t\treturn Object.assign(super.options, {\n\t\t\tbusinessHours: this.props.model.workingHours,\n\t\t});\n\t},\n});\n", "/** @odoo-module **/\n\nimport { CalendarFilterPanel } from \"@web/views/calendar/filter_panel/calendar_filter_panel\";\nimport { patch } from \"@web/core/utils/patch\";\n\n\npatch(CalendarFilterPanel.prototype, {\n    updateSelectCreateDialogProps(props) {\n        const updatedProps = super.updateSelectCreateDialogProps(props);\n        updatedProps.context = {\n            search_view_ref: 'hr_calendar.view_res_partner_filter_inherit_calendar',\n        };\n        return updatedProps;\n    }\n})\n", "import { Many2ManyAttendee } from \"@calendar/views/fields/many2many_attendee\";\nimport { patch } from \"@web/core/utils/patch\";\npatch(Many2ManyAttendee.prototype, {\n    get tags() {\n        const tags = super.tags;\n        if (this.props.record.data.unavailable_partner_ids) {\n            const unavailablePartnerIds = this.props.record.data.unavailable_partner_ids.records;\n            for (const tag of tags) {\n                if (unavailablePartnerIds.find((partner) => (tag.resId == partner.resId))) {\n                    tag.unavailableIcon = true;\n                }\n            }\n        }\n        return tags;\n    }\n})\n", "/** @odoo-module **/\n\nimport { patch } from \"@web/core/utils/patch\";\nimport { updateAccountOnMobileDevice } from \"@web_mobile/js/core/mixins\";\nimport { EmployeeProfileController } from \"@hr/views/profile_form_view\";\n\npatch(EmployeeProfileController.prototype, {\n    async onRecordSaved(record) {\n        await updateAccountOnMobileDevice();\n        return await super.onRecordSaved(...arguments);\n    },\n});\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from \"@web/core/user\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { useService } from \"@web/core/utils/hooks\";\n\n/**\n * Redirect to the sub employee kanban view.\n *\n * @private\n * @param {MouseEvent} event\n * @returns {Promise} action loaded\n *\n */\nexport function onEmployeeSubRedirect() {\n    const actionService = useService('action');\n    const orm = useService('orm');\n\n    return async (event) => {\n        const employeeId = parseInt(event.currentTarget.dataset.employeeId);\n        if (!employeeId) {\n            return {};\n        }\n        const type = event.currentTarget.dataset.type || 'direct';\n        // Get subordonates of an employee through a rpc call.\n        const subordinateIds = await rpc('/hr/get_subordinates', {\n            employee_id: employeeId,\n            subordinates_type: type,\n            context: user.context\n        });\n        let action = await orm.call('hr.employee', 'get_formview_action', [employeeId]);\n        action = {...action,\n            name: _t('Team'),\n            view_mode: 'kanban,list,form',\n            views: [[false, 'kanban'], [false, 'list'], [false, 'form']],\n            domain: [['id', 'in', subordinateIds]],\n            res_id: false,\n            context: {\n                default_parent_id: employeeId,\n            }\n        };\n        actionService.doAction(action);\n    };\n}\n", "/** @odoo-module */\n\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { user } from \"@web/core/user\";\nimport { onEmployeeSubRedirect } from './hooks';\nimport { Component, onWillStart, onWillRender, useState } from \"@odoo/owl\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nclass HrOrgChartPopover extends Component {\n    static template = \"hr_org_chart.hr_orgchart_emp_popover\";\n    static props = {\n        employee: Object,\n        close: Function,\n    };\n    async setup() {\n        super.setup();\n\n        this.orm = useService('orm');\n        this.actionService = useService(\"action\");\n        this._onEmployeeSubRedirect = onEmployeeSubRedirect();\n    }\n\n    /**\n     * Redirect to the employee form view.\n     *\n     * @private\n     * @param {MouseEvent} event\n     * @returns {Promise} action loaded\n     */\n    async _onEmployeeRedirect(employeeId) {\n        const action = await this.orm.call('hr.employee', 'get_formview_action', [employeeId]);\n        this.actionService.doAction(action); \n    }\n}\n\nexport class HrOrgChart extends Component {\n    static template = \"hr_org_chart.hr_org_chart\";\n    static props = {...standardFieldProps};\n    async setup() {\n        super.setup();\n\n        this.orm = useService('orm');\n        this.actionService = useService(\"action\");\n        this.popover = usePopover(HrOrgChartPopover);\n\n        this.state = useState({'employee_id': null});\n        this.lastParent = null;\n        this._onEmployeeSubRedirect = onEmployeeSubRedirect();\n\n        onWillStart(this.handleComponentUpdate.bind(this));\n        onWillRender(this.handleComponentUpdate.bind(this));\n    }\n\n    /**\n     * Called on start and on render\n     */\n    async handleComponentUpdate() {\n        this.employee = this.props.record.data;\n        // the widget is either dispayed in the context of a hr.employee form or a res.users form\n        this.state.employee_id = this.employee.employee_ids !== undefined ? this.employee.employee_ids.resIds[0] : this.props.record.resId;\n        const manager = this.employee.parent_id || this.employee.employee_parent_id;\n        const forceReload = this.lastRecord !== this.props.record || this.lastParent != manager;\n        this.lastParent = manager;\n        this.lastRecord = this.props.record;\n        await this.fetchEmployeeData(this.state.employee_id, forceReload);\n    }\n\n    async fetchEmployeeData(employeeId, force = false) {\n        if (!employeeId) {\n            this.managers = [];\n            this.children = [];\n            if (this.view_employee_id) {\n                this.render(true);\n            }\n            this.view_employee_id = null;\n        } else if (employeeId !== this.view_employee_id || force) {\n            this.view_employee_id = employeeId;\n            let orgData = await rpc(\n                '/hr/get_org_chart',\n                {\n                    employee_id: employeeId,\n                    context: user.context,\n                }\n            );\n            if (Object.keys(orgData).length === 0) {\n                orgData = {\n                    managers: [],\n                    children: [],\n                }\n            }\n            this.managers = orgData.managers;\n            this.children = orgData.children;\n            this.managers_more = orgData.managers_more;\n            this.self = orgData.self;\n            this.render(true);\n        }\n    }\n\n    _onOpenPopover(event, employee) {\n        this.popover.open(event.currentTarget, { employee });\n    }\n\n    /**\n     * Redirect to the employee form view.\n     *\n     * @private\n     * @param {MouseEvent} event\n     * @returns {Promise} action loaded\n     */\n    async _onEmployeeRedirect(employeeId) {\n        const action = await this.orm.call('hr.employee', 'get_formview_action', [employeeId]);\n        this.actionService.doAction(action); \n    }\n\n    async _onEmployeeMoreManager(managerId) {\n        await this.fetchEmployeeData(managerId);\n        this.state.employee_id = managerId;\n    }\n}\n\nexport const hrOrgChart = {\n    component: HrOrgChart,\n};\n\nregistry.category(\"fields\").add(\"hr_org_chart\", hrOrgChart);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { listView } from \"@web/views/list/list_view\";\n\nexport const LoyaltyCardListView = {\n    ...listView,\n    buttonTemplate: \"loyalty.LoyaltyCardListView.buttons\",\n};\n\nregistry.category(\"views\").add(\"loyalty_card_list_view\", LoyaltyCardListView);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { X2ManyField, x2ManyField } from \"@web/views/fields/x2many/x2many_field\";\n\nexport class LoyaltyX2ManyField extends X2ManyField {\n    static template = \"loyalty.LoyaltyX2ManyField\";\n}\n\nexport const loyaltyX2ManyField = {\n    ...x2ManyField,\n    component: LoyaltyX2ManyField,\n};\n\nregistry.category(\"fields\").add(\"loyalty_one2many\", loyaltyX2ManyField);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { listView } from \"@web/views/list/list_view\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Component, onWillStart } from \"@odoo/owl\";\n\nexport class LoyaltyActionHelper extends Component {\n    static template = \"loyalty.LoyaltyActionHelper\";\n    static props = [\"noContentHelp\"];\n    setup() {\n        this.orm = useService(\"orm\");\n        this.action = useService(\"action\");\n\n        onWillStart(async () => {\n            this.loyaltyTemplateData = await this.orm.call(\n                \"loyalty.program\",\n                \"get_program_templates\",\n                [],\n                {\n                    context: this.env.model.root.context,\n                },\n            );\n        });\n    }\n\n    async onTemplateClick(templateId) {\n        const action = await this.orm.call(\n            \"loyalty.program\",\n            \"create_from_template\",\n            [templateId],\n            {context: this.env.model.root.context},\n        );\n        if (!action) {\n            return;\n        }\n        this.action.doAction(action);\n    }\n};\n\nexport class LoyaltyListRenderer extends ListRenderer {\n    static template = \"loyalty.LoyaltyListRenderer\";\n    static components = {\n        ...LoyaltyListRenderer.components,\n        LoyaltyActionHelper,\n    };\n};\n\nexport const LoyaltyListView = {\n    ...listView,\n    Renderer: LoyaltyListRenderer,\n};\n\nregistry.category(\"views\").add(\"loyalty_program_list_view\", LoyaltyListView);\n", "/** @odoo-module **/\n\nimport mobile from \"@web_mobile/js/services/core\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { session } from \"@web/session\";\n\n//Send info only if client is mobile\nif (mobile.methods.getFCMKey) {\n    var registerDevice = function (fcm_project_id) {\n        mobile.methods.getFCMKey({\n            project_id: fcm_project_id,\n            inbox_action: session.inbox_action,\n        }).then(function (response) {\n            if (response.success) {\n                rpc('/web/dataset/call_kw/res.config.settings/register_device', {\n                    model: 'res.config.settings',\n                    method: 'register_device',\n                    args: [\n                        response.data.subscription_id,\n                        response.data.device_name,\n                        response.data.fcm_token_old,\n                    ],\n                    kwargs: {},\n                }).then(function (ocn_token) {\n                    if (mobile.methods.setOCNToken) {\n                        mobile.methods.setOCNToken({ocn_token: ocn_token});\n                    }\n                });\n            }\n        }).catch(e => console.error(e));\n    };\n    if (session.fcm_project_id) {\n        registerDevice(session.fcm_project_id);\n    } else {\n        rpc('/web/dataset/call_kw/res.config.settings/get_fcm_project_id', {\n            model: 'res.config.settings',\n            method: 'get_fcm_project_id',\n            args: [],\n            kwargs: {},\n        }).then(function (response) {\n            if (response) {\n                registerDevice(response);\n            }\n        });\n    }\n}\n", "import { patch } from \"@web/core/utils/patch\";\n\nimport { Thread } from '@mail/core/common/thread';\n\npatch(Thread.prototype, {\n    get displayMessages() {\n        let messages = (\n            this.props.order === 'asc' ?\n            this.props.thread.nonEmptyMessages :\n            [...this.props.thread.nonEmptyMessages].reverse()\n        );\n        if (!this.props.showNotificationMessages) {\n            messages = messages.filter(\n                (msg) => !['user_notification', 'notification'].includes(\n                    msg.message_type\n                )\n            );\n        }\n        return messages;\n    },\n});\n\nThread.props = [\n    ...Thread.props,\n    'showNotificationMessages?',\n];\nThread.defaultProps = {\n    ...Thread.defaultProps,\n    showNotificationMessages: true,\n};", "import { useState, useRef } from '@odoo/owl';\nimport { patch } from '@web/core/utils/patch';\nimport { browser } from \"@web/core/browser/browser\";\n\nimport { FormRenderer } from '@web/views/form/form_renderer';\n\npatch(FormRenderer.prototype, {\n    setup() {\n        super.setup();\n        this.chatterState = useState({\n            width: browser.localStorage.getItem('muk_web_chatter.width'),\n        });\n        this.chatterContainer = useRef('chatterContainer');\n    },\n    onStartChatterResize(ev) {\n        if (ev.button !== 0) {\n            return;\n        }\n        const initialX = ev.pageX;\n        const chatterElement = this.chatterContainer.el;\n        const initialWidth = chatterElement.offsetWidth;\n        const resizeStoppingEvents = [\n            'keydown', 'mousedown', 'mouseup'\n        ];\n        const resizePanel = (ev) => {\n            ev.preventDefault();\n            ev.stopPropagation();\n            const newWidth = Math.min(\n                Math.max(50, initialWidth - (ev.pageX - initialX)),\n                Math.max(chatterElement.parentElement.offsetWidth - 250, 250)\n            );\n            browser.localStorage.setItem('muk_web_chatter.width', newWidth);\n            this.chatterState.width = newWidth;\n        };\n        const stopResize = (ev) => {\n            ev.preventDefault();\n            ev.stopPropagation();\n            if (ev.type === 'mousedown' && ev.button === 0) {\n                return;\n            }\n            document.removeEventListener('mousemove', resizePanel, true);\n            resizeStoppingEvents.forEach((stoppingEvent) => {\n                document.removeEventListener(stoppingEvent, stopResize, true);\n            });\n            document.activeElement.blur();\n        };\n        document.addEventListener('mousemove', resizePanel, true);\n        resizeStoppingEvents.forEach((stoppingEvent) => {\n            document.addEventListener(stoppingEvent, stopResize, true);\n        });\n    },\n    onDoubleClickChatterResize(ev) {\n    \tbrowser.localStorage.removeItem('muk_web_chatter.width');\n        this.chatterState.width = false;\n    },\n});\n", "import { url } from \"@web/core/utils/urls\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { cookie } from \"@web/core/browser/cookie\";\n\nimport { HomeMenu } from \"@web_enterprise/webclient/home_menu/home_menu\";\n\npatch(HomeMenu.prototype, {\n    setup() {\n        super.setup();\n        if (\n    \t\tcookie.get(\"color_scheme\") === \"dark\" &&\n    \t\tthis.env.services.company.currentCompany.has_background_image_dark \n    \t) {\n        \tthis.backgroundImageUrl = url('/web/image', {\n                model: 'res.company',\n                field: 'background_image_dark',\n                id: this.env.services.company.currentCompany.id,\n            });\n        } else if (\n        \tthis.env.services.company.currentCompany.has_background_image_light\n        ) {\n        \tthis.backgroundImageUrl = url('/web/image', {\n                model: 'res.company',\n                field: 'background_image_light',\n                id: this.env.services.company.currentCompany.id,\n            });\n        }\n    },\n});\n", "import { AutoComplete } from \"@web/core/autocomplete/autocomplete\";\n\n\nexport class PartnerAutoComplete extends AutoComplete {\n    static template = \"partner_autocomplete.PartnerAutoComplete\";\n\n    setup() {\n        super.setup();\n        this.shouldSearchWorldwide = false;\n    }\n\n    // Override of AutoComplete\n    loadOptions(options, request) {\n        if (typeof options === \"function\") {\n            return options(request, this.shouldSearchWorldwide);\n        } else {\n            return options;\n        }\n    }\n\n    async searchWorldwide(ev){\n        this.shouldSearchWorldwide = true;\n        ev.preventDefault();\n        super.close();\n        super.open(true);\n    }\n}\n", "/** @odoo-module **/\n/* global checkVATNumber */\n\nimport { loadJS } from \"@web/core/assets\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { renderToMarkup } from \"@web/core/utils/render\";\nimport { getDataURLFromFile } from \"@web/core/utils/urls\";\n\n/**\n * Get list of companies via Autocomplete API\n *\n * @param {string} value\n * @returns {Promise}\n * @private\n */\nexport function usePartnerAutocomplete() {\n    const keepLastOdoo = new KeepLast();\n\n    const notification = useService(\"notification\");\n    const orm = useService(\"orm\");\n\n    function sanitizeVAT(value) {\n        return value ? value.replace(/[^A-Za-z0-9]/g, '') : '';\n    }\n\n    async function isVATNumber(value) {\n        // Lazyload jsvat only if the component is being used.\n        await loadJS(\"/partner_autocomplete/static/lib/jsvat.js\");\n\n        // checkVATNumber is defined in library jsvat.\n        // It validates that the input has a valid VAT number format\n        return checkVATNumber(sanitizeVAT(value));\n    }\n\n    function isGSTNumber(value) {\n        // Check if the input is a valid GST number.\n        let isGST = false;\n        if (value && value.length === 15) {\n            const allGSTinRe = [\n                /\\d{2}[a-zA-Z]{5}\\d{4}[a-zA-Z][1-9A-Za-z][Zz1-9A-Ja-j][0-9a-zA-Z]/, // Normal, Composite, Casual GSTIN\n                /\\d{4}[A-Z]{3}\\d{5}[UO]N[A-Z0-9]/, // UN/ON Body GSTIN\n                /\\d{4}[a-zA-Z]{3}\\d{5}NR[0-9a-zA-Z]/, // NRI GSTIN\n                /\\d{2}[a-zA-Z]{4}[a-zA-Z0-9]\\d{4}[a-zA-Z][1-9A-Za-z][DK][0-9a-zA-Z]/, // TDS GSTIN\n                /\\d{2}[a-zA-Z]{5}\\d{4}[a-zA-Z][1-9A-Za-z]C[0-9a-zA-Z]/ // TCS GSTIN\n            ];\n\n            isGST = allGSTinRe.some((re) => re.test(value));\n        }\n\n        return isGST;\n    }\n\n    async function autocomplete(value, queryCountryId) {\n        value = value.trim();\n        const isVAT = await isVATNumber(value);\n        if (isVAT){\n            value = sanitizeVAT(value);\n        }\n        const isGST = isGSTNumber(value);\n        return await getSuggestions(value, isVAT || isGST, queryCountryId);\n    }\n\n    /**\n     * Get enrichment data\n     *\n     * @param {Object} company\n     * @returns {Promise}\n     * @private\n     */\n    function enrichCompany(company) {\n        if (isGSTNumber(company.query)){\n            return orm.call('res.partner', 'enrich_by_gst', [company.query]);\n        }\n        return orm.call('res.partner', 'enrich_by_duns', [company.duns]);\n    }\n\n    function removeUselessFields(company, fieldsToKeep) {\n        // Delete attribute to avoid \"Field_changed\" errors (these fields will be populated in the form)\n        for (const field in company){\n            if (!fieldsToKeep.includes(field)){\n                delete company[field]\n            }\n        }\n        return company;\n    };\n\n    /**\n     * Get enriched data + logo before populating partner form\n     *\n     * @param {Object} company\n     * @returns {Promise}\n     */\n    function getCreateData(company, fieldsToKeep) {\n        return new Promise((resolve) => {\n            // Fetch additional company info via Autocomplete Enrichment API\n            const enrichPromise = enrichCompany(company);\n\n            // Get logo\n            const logoPromise = company.logoUrl ? getCompanyLogo(company.logoUrl) : false;\n            whenAll([enrichPromise, logoPromise]).then(([companyData, logoData]) => {\n\n                if (companyData.error) {\n                    if (companyData.error_message === 'Insufficient Credit') {\n                        notifyNoCredits();\n                    }\n                    else if (companyData.error_message === 'No Account Token') {\n                        notifyAccountToken();\n                    }\n                    else {\n                        notification.add(companyData.error_message);\n                    }\n                    companyData = {\n                        ...company,\n                        ...companyData,\n                    };\n                }\n\n                resolve({\n                    company: companyData,\n                    logo: logoData\n                });\n            })\n        });\n    }\n\n    async function fetchNoCaching(url) {\n        try {\n            const response = await browser.fetch(\n                url,\n                {\n                    method: 'GET',\n                    cache: 'no-cache',\n                }\n            );\n            return await response.json();\n        } catch {\n            return {}\n        }\n    }\n\n    /**\n     * Use Clearbit API to get the company logo if there is a match with the company name or domain\n     *\n     * @param {string} value\n     * @returns {Promise}\n     * @private\n     */\n    async function getClearbitLogoUrl(company) {\n        let clearbitData = await fetchNoCaching(encodeURI(`https://autocomplete.clearbit.com/v1/companies/suggest?query=${company.name}`));\n        if (!clearbitData.length) {\n            if (company.domain) {\n                clearbitData = await fetchNoCaching(encodeURI(`https://autocomplete.clearbit.com/v1/companies/suggest?query=${company.domain}`));\n            }\n            if (!clearbitData.length) {\n                return '';\n            }\n        }\n        const firstResult = clearbitData[0];\n        if (\n            firstResult.name.toLowerCase() === company.name.toLowerCase()\n            ||\n            (\n                company.domain !== undefined\n                &&\n                firstResult.domain === company.domain\n            )\n        ){\n            return firstResult.logo;\n        }\n        return '';\n    }\n\n    /**\n     * Get the company logo as Base 64 image from url\n     *\n     * @param {string} url\n     * @returns {Promise}\n     * @private\n     */\n    async function getCompanyLogo(logoUrl) {\n        try {\n            if (!logoUrl) {\n                return false;\n            }\n            const base64Image = await getBase64Image(logoUrl);\n            // base64Image equals \"data:\" if image not available on given url\n            return base64Image ? base64Image.replace(/^data:image[^;]*;base64,?/, '') : false;\n        }\n        catch {\n            return false;\n        }\n    }\n\n    /**\n     * Returns a promise which will be resolved with the base64 data of the\n     * image fetched from the given url.\n     *\n     * @private\n     * @param {string} url : the url where to find the image to fetch\n     * @returns {Promise}\n     */\n    function getBase64Image(url) {\n        return new Promise((resolve, reject) => {\n            const xhr = new XMLHttpRequest();\n            xhr.onload = () => {\n                getDataURLFromFile(xhr.response).then(resolve);\n            };\n            xhr.open('GET', url);\n            xhr.responseType = 'blob';\n            xhr.onerror = reject;\n            xhr.send();\n        });\n    }\n\n    /**\n     * Use Odoo Autocomplete API to return suggestions\n     *\n     * @param {string} value\n     * @param {boolean} isVAT\n     * @returns {Promise}\n     * @private\n     */\n    async function getSuggestions(value, isVAT, queryCountryId) {\n        const method = isVAT ? 'autocomplete_by_vat' : 'autocomplete_by_name';\n\n        const prom = orm.silent.call(\n            'res.partner',\n            method,\n            [value, queryCountryId],\n        );\n\n        const suggestions = await keepLastOdoo.add(prom);\n        await Promise.all(suggestions.map(async (suggestion) => {\n            suggestion.query = value;  // Save queried value (name, VAT) for later\n            suggestion.logoUrl = await getClearbitLogoUrl(suggestion);\n            suggestion.description = '';\n            if (suggestion.city){\n                suggestion.description += suggestion.city;\n            }\n            // Show country name only if searching worldwide\n            if (queryCountryId === 0 && suggestion.country_id && suggestion.country_id.display_name) {\n                suggestion.description +=  ', ' + suggestion.country_id.display_name;\n            }\n            return suggestion;\n        }));\n        return suggestions;\n    }\n\n    /**\n     * Utility to wait for multiple promises\n     * Promise.all will reject all promises whenever a promise is rejected\n     * This utility will continue\n     *\n     * @param {Promise[]} promises\n     * @returns {Promise}\n     * @private\n     */\n    function whenAll(promises) {\n        return Promise.all(promises.map((p) => {\n            return Promise.resolve(p);\n        }));\n    }\n\n    /**\n     * @private\n     * @returns {Promise}\n     */\n    async function notifyNoCredits() {\n        const url = await orm.call(\n            'iap.account',\n            'get_credits_url',\n            ['partner_autocomplete'],\n        );\n        const title = _t('Not enough credits for Partner Autocomplete');\n        const content = renderToMarkup('partner_autocomplete.InsufficientCreditNotification', {\n            credits_url: url\n        });\n        notification.add(content, {\n            title,\n        });\n    }\n\n    async function notifyAccountToken() {\n        const url = await orm.call(\n            'iap.account',\n            'get_config_account_url',\n            []\n        );\n        const title = _t('IAP Account Token missing');\n        if (url) {\n            const content = renderToMarkup('partner_autocomplete.AccountTokenMissingNotification', {\n                account_url: url\n            });\n            notification.add(content, {\n                title,\n            });\n        }\n        else {\n            notification.add(title);\n        }\n    }\n    return { autocomplete, getCreateData, removeUselessFields };\n}\n", "/** @odoo-module **/\n\nimport { useChildRef, useService } from \"@web/core/utils/hooks\";\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { CharField, charField } from \"@web/views/fields/char/char_field\";\nimport { useInputField } from \"@web/views/fields/input_field_hook\";\n\nimport { usePartnerAutocomplete } from \"@partner_autocomplete/js/partner_autocomplete_core\";\nimport { PartnerAutoComplete } from \"@partner_autocomplete/js/partner_autocomplete_component\";\n\nexport class PartnerAutoCompleteCharField extends CharField {\n    static template = \"partner_autocomplete.PartnerAutoCompleteCharField\";\n    static components = {\n        ...CharField.components,\n        PartnerAutoComplete,\n    };\n    setup() {\n        super.setup();\n\n        this.orm = useService(\"orm\");\n        this.partnerAutocomplete = usePartnerAutocomplete();\n\n        this.inputRef = useChildRef();\n        useInputField({ getValue: () => this.props.record.data[this.props.name] || \"\", parse: (v) => this.parse(v), ref: this.inputRef});\n    }\n\n    async validateSearchTerm(request) {\n        return request && request.length > 2;\n    }\n\n    get sources() {\n        return [\n            {\n                options: async (request, shouldSearchWorldWide) => {\n                    if (await this.validateSearchTerm(request)) {\n                        let queryCountryId = this.props.record.data?.country_id ? this.props.record.data.country_id[0] : false;\n                        if (shouldSearchWorldWide){\n                            queryCountryId = 0;\n                        }\n                        const suggestions = await this.partnerAutocomplete.autocomplete(request, queryCountryId);\n                        suggestions.forEach((suggestion) => {\n                            suggestion.classList = \"partner_autocomplete_dropdown_char\";\n                        });\n                        return suggestions;\n                    }\n                    else {\n                        return [];\n                    }\n                },\n                optionTemplate: \"partner_autocomplete.DropdownOption\",\n                placeholder: _t('Searching Autocomplete...'),\n            },\n        ];\n    }\n\n    async onSelect(option) {\n        let data = await this.partnerAutocomplete.getCreateData(Object.getPrototypeOf(option));\n        if (!data?.company) {\n            return;\n        }\n\n        if (data.logo) {\n            const logoField = this.props.record.resModel === 'res.partner' ? 'image_1920' : 'logo';\n            data.company[logoField] = data.logo;\n        }\n\n        // Format the many2one fields\n        const many2oneFields = ['country_id', 'state_id'];\n        many2oneFields.forEach((field) => {\n            if (data.company[field]) {\n                data.company[field] = [data.company[field].id, data.company[field].display_name];\n            }\n        });\n\n        // Save UNSPSC codes (tags)\n        const unspsc_codes = data.company.unspsc_codes\n\n        // Delete useless fields before updating record\n        data.company = this.partnerAutocomplete.removeUselessFields(data.company, Object.keys(this.props.record.fields));\n\n        // Update record with retrieved values\n        await this.props.record.update({name: data.company.name});  // Needed otherwise name it is not saved\n        await this.props.record.update(data.company);\n\n        // Add UNSPSC codes (tags)\n        if (this.props.record.resModel === 'res.partner' && unspsc_codes) {\n            // We must first save the record so that we can then create the tags (many2many)\n            const saved = await this.props.record.save();\n            if (saved){\n                await this.props.record.load();\n                await this.orm.call(\"res.partner\", \"iap_partner_autocomplete_add_tags\", [this.props.record.resId, unspsc_codes]);\n                await this.props.record.load();\n            }\n        }\n        if (this.props.setDirty) {\n            this.props.setDirty(false);\n        }\n    }\n}\n\nexport const partnerAutoCompleteCharField = {\n    ...charField,\n    component: PartnerAutoCompleteCharField,\n};\n\nregistry.category(\"fields\").add(\"field_partner_autocomplete\", partnerAutoCompleteCharField);\n", "/** @odoo-module **/\n\nimport { Many2XAutocomplete } from '@web/views/fields/relational_utils';\nimport { Many2OneField, many2OneField } from '@web/views/fields/many2one/many2one_field';\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\n\nimport { usePartnerAutocomplete } from \"@partner_autocomplete/js/partner_autocomplete_core\";\nimport { PartnerAutoComplete } from \"@partner_autocomplete/js/partner_autocomplete_component\";\n\nexport class PartnerMany2XAutocomplete extends Many2XAutocomplete {\n    static template = \"partner_autocomplete.PartnerAutoCompleteMany2XField\";\n    static components = {\n        ...Many2XAutocomplete.components,\n        PartnerAutoComplete,\n    };\n\n    setup() {\n        super.setup();\n        this.partnerAutocomplete = usePartnerAutocomplete();\n    }\n\n    validateSearchTerm(request) {\n        return request && request.length > 2;\n    }\n\n    get sources() {\n        const sources = super.sources;\n        if (!this.props.canCreate) {\n            return sources;\n        }\n        return sources.concat(\n            {\n                options: async (request, shouldSearchWorldWide) => {\n                    if (this.validateSearchTerm(request)) {\n                        let queryCountryId = false;\n                        if (shouldSearchWorldWide){\n                            queryCountryId = 0;\n                        }\n                        const suggestions = await this.partnerAutocomplete.autocomplete(request, queryCountryId);\n                        suggestions.forEach((suggestion) => {\n                            suggestion.classList = \"partner_autocomplete_dropdown_many2one\";\n                            suggestion.isFromPartnerAutocomplete = true;\n                        });\n                        return suggestions;\n                    }\n                    else {\n                        return [];\n                    }\n                },\n                optionTemplate: \"partner_autocomplete.DropdownOption\",\n                placeholder: _t(\"Searching Autocomplete...\"),\n            },\n        );\n    }\n\n    async onSelect(option, params) {\n        if (option.isFromPartnerAutocomplete) {  // Checks that it is a partner autocomplete option\n            const data = await this.partnerAutocomplete.getCreateData(Object.getPrototypeOf(option));\n            if (!data?.company) {\n                return;\n            }\n            let context = {\n                'default_is_company': true\n            };\n\n            for (const [key, val] of Object.entries(data.company)) {\n                context['default_' + key] = val && val.id ? val.id : val;\n            }\n\n            if (data.logo) {\n                context.default_image_1920 = data.logo;\n            }\n            return this.openMany2X({ context });\n        }\n        else {\n            return super.onSelect(option, params);\n        }\n    }\n\n}\n\nPartnerMany2XAutocomplete.props = {\n    ...Many2XAutocomplete.props,\n    canCreate: { type: Boolean, optional: true },\n}\n\nexport class PartnerAutoCompleteMany2one extends Many2OneField {\n    static components = {\n        ...Many2OneField.components,\n        Many2XAutocomplete: PartnerMany2XAutocomplete,\n    };\n    static props = {\n        ...Many2OneField.props,\n        canCreate: this.props.canCreate,\n    };\n    get Many2XAutocompleteProps() {\n        return {\n            ...super.Many2XAutocompleteProps,\n            canCreate: this.props.canCreate,\n        };\n    }\n}\n\nexport const partnerAutoCompleteMany2one = {\n    ...many2OneField,\n    component: PartnerAutoCompleteMany2one,\n};\n\nregistry.category(\"fields\").add(\"res_partner_many2one\", partnerAutoCompleteMany2one);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { session } from \"@web/session\";\n\nexport const companyAutocompleteService = {\n    dependencies: [\"orm\", \"company\"],\n\n    start(env, { orm, company }) {\n        if (session.iap_company_enrich) {\n            const currentCompanyId = company.currentCompany.id;\n            orm.silent.call(\"res.company\", \"iap_enrich_auto\", [currentCompanyId], {});\n        }\n    },\n};\n\nregistry\n    .category(\"services\")\n    .add(\"partner_autocomplete.companyAutocomplete\", companyAutocompleteService);\n", "import { registry } from \"@web/core/registry\";\nimport { CharField, charField } from \"@web/views/fields/char/char_field\";\nimport { scanBarcode } from \"@web/core/barcode/barcode_dialog\";\n\nexport class BarcodeScannerWidget extends CharField {\n    static template = \"product_barcodelookup.barcodeformbarcode\";\n    setup() {\n        super.setup();\n    }\n    async onBarcodeBtnClick() {\n        const barcode = await scanBarcode(this.env);\n        if (barcode) {\n            await this.props.record.update({\n                barcode: barcode,\n            });\n        }\n    }\n}\n\nexport const barcodeScannerWidget = {\n    ...charField,\n    component: BarcodeScannerWidget,\n};\n\nregistry.category(\"fields\").add(\"productScanner\", barcodeScannerWidget);\n", "import { Dialog } from '@web/core/dialog/dialog';\nimport { formatMonetary } from \"@web/views/fields/formatters\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { Component, onMounted, markup, useRef } from \"@odoo/owl\";\n\nexport class ProductMatrixDialog extends Component {\n    static template = \"product_matrix.dialog\";\n    static props = {\n        header: { type: Object },\n        rows: { type: Object },\n        editedCellAttributes: { type: String },\n        product_template_id: { type: Number },\n        record: { type: Object },\n        close: { type: Function },\n    };\n    static components = { Dialog };\n\n    setup() {\n        this.size = 'xl';\n\n        const productMatrixRef = useRef('productMatrix');\n        useHotkey(\"enter\", () => this._onConfirm(), {\n            /***\n             * By default, Hotkeys don't work in input fields. As the matrix table is composed of\n             * input fields, the `bypassEditableProtection` param will allow Hotkeys to work from\n             * the input fields.\n             *\n             * To avoid triggering the confirmation when pressing 'enter' on the close or the\n             * discard button, we only set the hotkey area on the matrix table.\n             */\n            bypassEditableProtection: true,\n            area: () => productMatrixRef.el,\n        });\n\n        onMounted(() => {\n            if(this.props.editedCellAttributes.length) {\n                const inputs = document.getElementsByClassName('o_matrix_input');\n                const relevantInput = Array.from(inputs).filter((matrixInput) =>\n                    matrixInput.attributes.ptav_ids.nodeValue === this.props.editedCellAttributes\n                )[0];\n                if (relevantInput) {\n                    relevantInput.select();\n                } else {\n                    // Based on the record creation, it may ignore the 'no_variant' attributes\n                    // (e.g. from a stock.move), thus finding no match in the matrix.\n                    inputs[0].select();\n                }\n            } else {\n                document.getElementsByClassName('o_matrix_input')[0].select();\n            }\n        });\n    }\n\n    _format({price, currency_id}) {\n        if (!price) { return \"\"; }\n        const sign = price < 0 ? '-' : '+';\n        const formatted = formatMonetary(\n            Math.abs(price),\n            {\n                currencyId: currency_id,\n            },\n        );\n        return markup(`&nbsp;${sign}&nbsp;${formatted}&nbsp;`);\n    }\n\n    _onConfirm() {\n        const inputs = document.getElementsByClassName('o_matrix_input');\n        let matrixChanges = [];\n        for (let matrixInput of inputs) {\n            if (matrixInput.value && matrixInput.value !== matrixInput.attributes.value.nodeValue) {\n                matrixChanges.push({\n                    qty: parseFloat(matrixInput.value),\n                    ptav_ids: matrixInput.attributes.ptav_ids.nodeValue.split(\",\").map(\n                        id => parseInt(id)\n                    ),\n                });\n            }\n        }\n        if (matrixChanges.length > 0) {\n            // NB: server also removes current line opening the matrix\n            this.props.record.update({\n                grid: JSON.stringify({\n                    changes: matrixChanges,\n                    product_template_id: this.props.product_template_id\n                }),\n                grid_update: true // to say that the changes to grid have to be applied to the SO.\n            });\n        }\n        this.props.close();\n    }\n}\n", "/** @odoo-module **/\nimport { patch } from \"@web/core/utils/patch\";\n\nimport { BankRecKanbanController } from \"@account_accountant/components/bank_reconciliation/kanban\";\n\npatch(BankRecKanbanController.prototype, {\n\n    // -----------------------------------------------------------------------------\n    // RPC\n    // -----------------------------------------------------------------------------\n\n    async actionRedirectToSaleOrders(){\n        await this.execProtectedBankRecAction(async () => {\n            await this.withNewState(async (newState) => {\n                const { return_todo_command: actionData } = await this.onchange(newState, \"redirect_to_matched_sale_orders\");\n                if(actionData){\n                    this.action.doAction(actionData);\n                }\n            });\n        });\n    },\n\n});\n", "import { registry } from \"@web/core/registry\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nimport { Component, onWillRender, toRaw } from \"@odoo/owl\";\n\nexport class LoyaltyDataField extends Component {\n    static template = \"sale_loyalty.LoyaltyDataField\";\n    static props = {\n        ...standardFieldProps,\n    };\n\n    setup() {\n        onWillRender(() => this.formatData());\n    }\n\n    formatData() {\n        const LoyaltyCardData = this.props.record.data[this.props.name];\n        this.LoyaltyCardData = Object.keys(LoyaltyCardData).length && toRaw(LoyaltyCardData);\n    }\n\n}\n\nexport const loyaltyDataField = {\n    component: LoyaltyDataField,\n};\n\nregistry.category(\"fields\").add(\"loyalty_data_field\", loyaltyDataField);\n", "import { Component, useEffect, useState } from \"@odoo/owl\";\nimport {\n    CustomFieldCard\n} from \"@sale_pdf_quote_builder/js/custom_content_kanban_like_widget/custom_field_card/custom_field_card\";\nimport { x2ManyCommands } from \"@web/core/orm_service\";\nimport { registry } from '@web/core/registry';\nimport { useService } from \"@web/core/utils/hooks\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\n\nexport class CustomContentKanbanLikeWidget extends Component {\n    static components = { CustomFieldCard };\n    static template = \"sale_pdf_quote_builder.CustomContentKanbanLike\";\n    static props = {\n        ...standardWidgetProps,\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.state = useState({\n            headers: {},\n            lines: {},\n            footers: {},\n        });\n\n        // Initialize the state and update available documents when updating the quotation template.\n        useEffect((saleOrderTemplate) => {\n            this.updateState();\n        }, () => [this.props.record.data.sale_order_template_id]);\n    }\n\n    async updateState() {\n        const saved = await this.props.record.save();  // To display documents of potentially unsaved SOL.\n        if (saved) {  // do not fetch wrong form data if record was not saved.\n            const { headers, lines, footers } = await this.orm.call(\n                'sale.order', 'get_update_included_pdf_params', [this.props.record.resId]\n            )\n            this.state.headers = headers;\n            this.state.lines = lines;\n            this.state.footers = footers;\n        }\n    }\n\n    updateJson() {\n        const selectedHeaders = this.state.headers.files.filter(f => f.is_selected);\n        const selectedFooters = this.state.footers.files.filter(f => f.is_selected);\n        const value = JSON.stringify({\n            'header': Object.assign({}, ...selectedHeaders.map(header => {\n                return {\n                    [header.id]: {\n                        document_name: header.name,\n                        custom_form_fields: Object.assign({}, ...header.custom_form_fields.map(\n                            formField => ({[formField.name]: formField.value})\n                        )),\n                    }\n            }})),\n            'line': Object.assign({}, ...this.state.lines.map(line => {\n                return {\n                    [line.id]: Object.assign({}, ...line.files.filter(f => f.is_selected).map(doc => {\n                        return {\n                            [doc.id]: {\n                                document_name: doc.name,\n                                custom_form_fields: Object.assign({}, ...doc.custom_form_fields.map(\n                                    formField => ({[formField.name]: formField.value})\n                                )),\n                            }\n                    }})),\n            }})),\n            'footer': Object.assign({}, ...selectedFooters.map(footer => {\n                return {\n                    [footer.id]: {\n                        document_name: footer.name,\n                        custom_form_fields: Object.assign({}, ...footer.custom_form_fields.map(\n                            formField => ({[formField.name]: formField.value})\n                        )),\n                    }\n            }})),\n        })\n        this.props.record.update({ ['customizable_pdf_form_fields']: value });\n    }\n\n    async saveProductDocument(lineId, docId, isSelected) {\n        const sol = this.props.record.data.order_line.records.find(\n            sol => sol.resId === lineId\n        );\n        sol._noUpdateParent = true; // Ensure that no rpc will be made to save the changes\n        if (isSelected) {\n            // save is needed to ensure that no onChange call will be made\n            await sol.update({product_document_ids: [x2ManyCommands.link(docId)]}, { save: true });\n        } else {\n            // save is needed to ensure that no onChange call will be made\n            await sol.update({product_document_ids: [x2ManyCommands.unlink(docId)]}, { save: true });\n        }\n        await this.props.record.data.order_line._onUpdate({withoutOnchange: true});\n        this.updateJson();\n    };\n\n    async saveQuotationDocument(docId, isSelected) {\n        if (isSelected) {\n            await this.props.record.update({\n                quotation_document_ids: [\n                    x2ManyCommands.link(docId),\n                ],\n            });\n        } else {\n            await this.props.record.update({\n                quotation_document_ids: [\n                    x2ManyCommands.unlink(docId),\n                ],\n            });\n        }\n        this.updateJson();\n    };\n}\n\nexport const customContentKanbanLikeWidget = {\n    component: CustomContentKanbanLikeWidget,\n};\n\nregistry.category(\"view_widgets\").add(\n    \"customContentKanbanLikeWidget\", customContentKanbanLikeWidget\n);\n", "/** @odoo-module **/\n\nimport { Component, useRef } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useAutoresize } from \"@web/core/utils/autoresize\";\n\nexport class CustomFieldCard extends Component {\n    static template = \"sale_pdf_quote_builder.customFieldCard\";\n    static props = {\n        name: String,\n        value: String,\n        onChange: Function,\n    };\n\n    setup() {\n        this.customFormFieldTextAreaRef = useRef('customFieldCardTextArea');\n        this.placeholder = _t(\"Click to write content for the PDF quote...\");\n        useAutoresize(this.customFormFieldTextAreaRef);\n    }\n\n    expandTextArea(ev) {\n        const textarea = ev.target;\n        textarea.style.height = textarea.scrollHeight+'px';\n    }\n}\n", "/** @odoo-module **/\n\nimport { onWillRender } from \"@odoo/owl\";\nimport { UploadButton } from '@product/js/product_document_kanban/upload_button/upload_button';\nimport { KanbanController } from '@web/views/kanban/kanban_controller';\n\nexport class QuotationDocumentKanbanController extends KanbanController {\n    static components = { ...KanbanController.components, UploadButton };\n\n    setup() {\n        super.setup();\n        this.uploadRoute = '/sale_pdf_quote_builder/quotation_document/upload';\n        this.allowedMIMETypes='application/pdf';\n        onWillRender(() => {\n            this.formData = {\n                'allowed_company_ids': JSON.stringify(this.props.context.allowed_company_ids),\n            };\n        });\n    }\n}\n", "import {\n    productDocumentKanbanView\n} from '@product/js/product_document_kanban/product_document_kanban_view';\nimport {\n    QuotationDocumentKanbanController\n} from '@sale_pdf_quote_builder/js/quotation_document_kanban/quotation_document_kanban_controller';\nimport { registry } from '@web/core/registry';\n\nexport const quotationDocumentKanbanView = {\n    ...productDocumentKanbanView,\n    Controller: QuotationDocumentKanbanController,\n};\n\nregistry.category('views').add('quotation_document_kanban', quotationDocumentKanbanView);\n", "/** @odoo-module **/\n\nimport {\n    ProductDocumentKanbanRenderer\n} from \"@product/js/product_document_kanban/product_document_kanban_renderer\";\nimport { UploadButton } from '@product/js/product_document_kanban/upload_button/upload_button';\nimport { registry } from '@web/core/registry';\nimport { X2ManyField, x2ManyField } from '@web/views/fields/x2many/x2many_field';\nimport { onWillRender } from \"@odoo/owl\";\n\nexport class QuotationDocumentX2ManyField extends X2ManyField {\n    static template = 'sale_pdf_quote_builder.QuotationDocumentX2ManyField';\n    static components = {\n        ...X2ManyField.components,\n        UploadButton,\n        KanbanRenderer: ProductDocumentKanbanRenderer,\n    };\n\n    setup() {\n        super.setup();\n        this.uploadRoute = '/sale_pdf_quote_builder/quotation_document/upload';\n        this.allowedMIMETypes='application/pdf';\n        onWillRender(() => {\n            this.formData = {\n                'sale_order_template_id': this.props.record.resId,\n            };\n        });\n    }\n}\n\nexport const quotationDocumentX2ManyField = {\n    ...x2ManyField,\n    component: QuotationDocumentX2ManyField,\n};\n\nregistry.category('fields').add('quotation_document_many2many', quotationDocumentX2ManyField);\n", "/** @odoo-module **/\n\nimport { patch } from \"@web/core/utils/patch\";\nimport { SaleOrderLineProductField } from '@sale/js/sale_product_field';\nimport { ProductMatrixDialog } from \"@product_matrix/js/product_matrix_dialog\";\nimport { useService } from \"@web/core/utils/hooks\";\n\npatch(SaleOrderLineProductField.prototype, {\n\n    setup() {\n        super.setup(...arguments);\n        this.dialog = useService(\"dialog\");\n    },\n\n    async _openGridConfigurator(edit=false) {\n        const saleOrderRecord = this.props.record.model.root;\n\n        // fetch matrix information from server;\n        await saleOrderRecord.update({\n            grid_product_tmpl_id: this.props.record.data.product_template_id,\n        });\n\n        let updatedLineAttributes = [];\n        if (edit) {\n            // provide attributes of edited line to automatically focus on matching cell in the matrix\n            for (let ptnvav of this.props.record.data.product_no_variant_attribute_value_ids.records) {\n                updatedLineAttributes.push(ptnvav.resId);\n            }\n            for (let ptav of this.props.record.data.product_template_attribute_value_ids.records) {\n                updatedLineAttributes.push(ptav.resId);\n            }\n            updatedLineAttributes.sort((a, b) => { return a - b; });\n        }\n\n        this._openMatrixConfigurator(\n            saleOrderRecord.data.grid,\n            this.props.record.data.product_template_id[0],\n            updatedLineAttributes,\n        );\n\n        if (!edit) {\n            // remove new line used to open the matrix\n            saleOrderRecord.data.order_line.delete(this.props.record);\n        }\n    },\n\n    async _openProductConfigurator(edit=false) {\n        if (edit && this.props.record.data.product_add_mode == 'matrix') {\n            this._openGridConfigurator(true);\n        } else {\n            super._openProductConfigurator(...arguments);\n        }\n    },\n\n    /**\n     * Triggers Matrix Dialog opening\n     *\n     * @param {String} jsonInfo matrix dialog content\n     * @param {integer} productTemplateId product.template id\n     * @param {editedCellAttributes} list of product.template.attribute.value ids\n     *  used to focus on the matrix cell representing the edited line.\n     *\n     * @private\n    */\n    _openMatrixConfigurator(jsonInfo, productTemplateId, editedCellAttributes) {\n        const infos = JSON.parse(jsonInfo);\n        this.dialog.add(ProductMatrixDialog, {\n            header: infos.header,\n            rows: infos.matrix,\n            editedCellAttributes: editedCellAttributes.toString(),\n            product_template_id: productTemplateId,\n            record: this.props.record.model.root,\n        });\n    },\n});\n", "/** @odoo-module */\nimport { ProductCatalogKanbanRecord } from \"@product/product_catalog/kanban_record\";\nimport { ProductCatalogSaleOrderLine } from \"./sale_order_line/sale_order_line\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(ProductCatalogKanbanRecord.prototype, {\n    updateQuantity(quantity) {\n        if (this.env.orderResModel !== \"sale.order\" || this.productCatalogData.productType == \"service\") {\n            super.updateQuantity(...arguments);\n        } else if (\n            this.productCatalogData.quantity === this.productCatalogData.deliveredQty &&\n            quantity < this.productCatalogData.quantity\n        ) {\n            // This condition is only triggered when the product was already at the minimum quantity\n            // possible, as stated in the sale_stock module, then the user inputs a quantity lower\n            // than this limit, in this case we need the record to forcefully update the record.\n            this.props.record.load();\n            this.props.record.model.notify();\n        } else {\n            super.updateQuantity(Math.max(quantity, this.productCatalogData.deliveredQty));\n        }\n    },\n\n    get orderLineComponent() {\n        if (this.env.orderResModel === \"sale.order\") {\n            return ProductCatalogSaleOrderLine;\n        }\n        return super.orderLineComponent;\n    },\n});\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ProductCatalogOrderLine } from \"@product/product_catalog/order_line/order_line\";\n\nexport class ProductCatalogSaleOrderLine extends ProductCatalogOrderLine {\n    static props = {\n        ...ProductCatalogOrderLine.props,\n        deliveredQty: Number,\n    }\n\n    get disableRemove() {\n        return this.props.quantity === this.props.deliveredQty;\n    }\n\n    get disabledButtonTooltip() {\n        if (this.disableRemove) {\n            return _t(\"The ordered quantity cannot be decreased below the amount already delivered. Instead, create a return in your inventory.\");\n        }\n        return super.disabledButtonTooltip;\n    }\n}\n", "/** @odoo-module **/\nimport { formatMonetary } from \"@web/views/fields/formatters\";\nimport { patch } from \"@web/core/utils/patch\";\n\nimport { ForecastedDetails } from \"@stock/stock_forecasted/forecasted_details\";\n\npatch(ForecastedDetails.prototype, {\n    _formatMonetary(num, currencyId){\n        return formatMonetary(num,{ currencyId: currencyId});\n    }\n});\n", "/** @odoo-module **/\n\nimport { formatDateTime } from \"@web/core/l10n/dates\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { Component, onWillRender } from \"@odoo/owl\";\nimport { standardWidgetProps } from \"@web/views/widgets/standard_widget_props\";\n\nexport class QtyAtDatePopover extends Component {\n    static template = \"sale_stock.QtyAtDatePopover\";\n    static props = {\n        record: Object,\n        calcData: Object,\n        close: Function,\n    };\n    setup() {\n        this.actionService = useService(\"action\");\n    }\n\n    openForecast() {\n        this.actionService.doAction(\"stock.stock_forecasted_product_product_action\", {\n            additionalContext: {\n                active_model: 'product.product',\n                active_id: this.props.record.data.product_id[0],\n                warehouse_id: this.props.record.data.warehouse_id && this.props.record.data.warehouse_id[0],\n                move_to_match_ids: this.props.record.data.move_ids.records.map(record => record.resId),\n                sale_line_to_match_id: this.props.record.resId,\n            },\n        });\n    }\n}\n\n\nexport class QtyAtDateWidget extends Component {\n    static components = { Popover: QtyAtDatePopover };\n    static template = \"sale_stock.QtyAtDate\";\n    static props = {...standardWidgetProps};\n    setup() {\n        this.popover = usePopover(this.constructor.components.Popover, { position: \"top\" });\n        this.calcData = {};\n        onWillRender(() => {\n            this.initCalcData();\n        })\n    }\n\n    initCalcData() {\n        // calculate data not in record\n        const { data } = this.props.record;\n        if (data.scheduled_date) {\n            // TODO: might need some round_decimals to avoid errors\n            if (data.state === 'sale') {\n                this.calcData.will_be_fulfilled = data.free_qty_today >= data.qty_to_deliver;\n            } else {\n                this.calcData.will_be_fulfilled = data.virtual_available_at_date >= data.qty_to_deliver;\n            }\n            this.calcData.will_be_late = data.forecast_expected_date && data.forecast_expected_date > data.scheduled_date;\n            if (['draft', 'sent'].includes(data.state)) {\n                // Moves aren't created yet, then the forecasted is only based on virtual_available of quant\n                this.calcData.forecasted_issue = !this.calcData.will_be_fulfilled && !data.is_mto;\n            } else {\n                // Moves are created, using the forecasted data of related moves\n                this.calcData.forecasted_issue = !this.calcData.will_be_fulfilled || this.calcData.will_be_late;\n            }\n        }\n    }\n\n    updateCalcData() {\n        // popup specific data\n        const { data } = this.props.record;\n        if (!data.scheduled_date) {\n            return;\n        }\n        this.calcData.delivery_date = formatDateTime(data.scheduled_date, { format: localization.dateFormat });\n        if (data.forecast_expected_date) {\n            this.calcData.forecast_expected_date_str = formatDateTime(data.forecast_expected_date, { format: localization.dateFormat });\n        }\n    }\n\n    showPopup(ev) {\n        this.updateCalcData();\n        this.popover.open(ev.currentTarget, {\n            record: this.props.record,\n            calcData: this.calcData,\n        });\n    }\n}\n\nexport const qtyAtDateWidget = {\n    component: QtyAtDateWidget,\n};\nregistry.category(\"view_widgets\").add(\"qty_at_date_widget\", qtyAtDateWidget);\n", "/** @odoo-module */\n\nimport { Failure } from \"@mail/core/common/failure_model\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Failure.prototype, {\n    get iconSrc() {\n        if (this.type === \"snail\") {\n            return \"/snailmail/static/img/snailmail_failure.png\";\n        }\n        return super.iconSrc;\n    },\n    get body() {\n        if (this.type === \"snail\") {\n            if (this.notifications.length === 1 && this.lastMessage?.thread) {\n                return _t(\n                    \"An error occurred when sending a letter with Snailmail on \u201c%(record_name)s\u201d\",\n                    { record_name: this.lastMessage.thread.name }\n                );\n            }\n            return _t(\"An error occurred when sending a letter with Snailmail.\");\n        }\n        return super.body;\n    },\n});\n", "/** @odoo-module */\n\nimport { Notification } from \"@mail/core/common/notification_model\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { _t } from \"@web/core/l10n/translation\";\n\npatch(Notification.prototype, {\n    get icon() {\n        if (this.notification_type === \"snail\") {\n            return \"fa fa-paper-plane\";\n        }\n        return super.icon;\n    },\n\n    get statusIcon() {\n        if (this.notification_type === \"snail\") {\n            switch (this.notification_status) {\n                case \"sent\":\n                    return \"fa fa-check\";\n                case \"ready\":\n                    return \"fa fa-clock-o\";\n                case \"canceled\":\n                    return \"fa fa-trash-o\";\n                default:\n                    return \"fa fa-exclamation text-danger\";\n            }\n        }\n        return super.statusIcon;\n    },\n\n    get statusTitle() {\n        if (this.notification_type === \"snail\") {\n            switch (this.notification_status) {\n                case \"sent\":\n                    return _t(\"Sent\");\n                case \"ready\":\n                    return _t(\"Awaiting Dispatch\");\n                case \"canceled\":\n                    return _t(\"Cancelled\");\n                default:\n                    return _t(\"Error\");\n            }\n        }\n        return super.statusTitle;\n    },\n});\n", "/** @odoo-module */\n\nimport { Message } from \"@mail/core/common/message\";\n\nimport { SnailmailError } from \"./snailmail_error\";\nimport { SnailmailNotificationPopover } from \"./snailmail_notification_popover\";\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(Message.prototype, {\n    onClickFailure() {\n        if (this.message.message_type === \"snailmail\") {\n            const failureType = this.message.notifications[0].failure_type;\n            switch (failureType) {\n                case \"sn_credit\":\n                case \"sn_trial\":\n                case \"sn_price\":\n                case \"sn_error\":\n                    this.env.services.dialog.add(SnailmailError, {\n                        failureType: failureType,\n                        messageId: this.message.id,\n                    });\n                    break;\n                case \"sn_fields\":\n                    this.openMissingFieldsLetterAction();\n                    break;\n                case \"sn_format\":\n                    this.openFormatLetterAction();\n                    break;\n            }\n        } else {\n            super.onClickFailure(...arguments);\n        }\n    },\n\n    async openMissingFieldsLetterAction() {\n        const letterIds = await this.env.services.orm.searchRead(\n            \"snailmail.letter\",\n            [[\"message_id\", \"=\", this.message.id]],\n            [\"id\"]\n        );\n        this.env.services.action.doAction(\n            \"snailmail.snailmail_letter_missing_required_fields_action\",\n            {\n                additionalContext: {\n                    default_letter_id: letterIds[0].id,\n                },\n            }\n        );\n    },\n\n    openFormatLetterAction() {\n        this.env.services.action.doAction(\"snailmail.snailmail_letter_format_error_action\", {\n            additionalContext: {\n                message_id: this.message.id,\n            },\n        });\n    },\n});\n\nMessage.components = {\n    ...Message.components,\n    Popover: SnailmailNotificationPopover,\n    SnailmailError,\n};\n", "import { Component } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\n\nimport { useService } from \"@web/core/utils/hooks\";\n\nexport class SnailmailError extends Component {\n    static components = { Dialog };\n    static props = [\"close\", \"failureType\", \"messageId\"];\n    static template = \"snailmail.SnailmailError\";\n\n    setup() {\n        this.orm = useService(\"orm\");\n    }\n\n    async fetchSnailmailCreditsUrl() {\n        return await this.orm.call(\"iap.account\", \"get_credits_url\", [\"snailmail\"]);\n    }\n\n    async fetchSnailmailCreditsUrlTrial() {\n        return await this.orm.call(\"iap.account\", \"get_credits_url\", [\"snailmail\", \"\", 0, true]);\n    }\n\n    async onClickResendLetter() {\n        await this.orm.call(\"mail.message\", \"send_letter\", [[this.props.messageId]]);\n        this.props.close();\n    }\n\n    async onClickCancelLetter() {\n        await this.orm.call(\"mail.message\", \"cancel_letter\", [[this.props.messageId]]);\n        this.props.close();\n    }\n\n    get snailmailCreditsUrl() {\n        return this.fetchSnailmailCreditsUrl();\n    }\n\n    get snailmailCreditsUrlTrial() {\n        return this.fetchSnailmailCreditsUrlTrial();\n    }\n}\n", "/* @odoo-module */\n\nimport { Component } from \"@odoo/owl\";\n\nexport class SnailmailNotificationPopover extends Component {\n    static template = \"snailmail.SnailmailNotificationPopover\";\n    static props = [\"message\", \"close?\"];\n}\n", "/** @odoo-module */\n\nimport { MessagingMenu } from \"@mail/core/public_web/messaging_menu\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(MessagingMenu.prototype, {\n    openFailureView(failure) {\n        if (failure.type !== \"snail\") {\n            return super.openFailureView(failure);\n        }\n        this.env.services.action.doAction({\n            name: _t(\"Snailmail Failures\"),\n            type: \"ir.actions.act_window\",\n            view_mode: \"kanban,list,form\",\n            views: [\n                [false, \"kanban\"],\n                [false, \"list\"],\n                [false, \"form\"],\n            ],\n            target: \"current\",\n            res_model: failure.resModel,\n            domain: [[\"message_ids.snailmail_error\", \"=\", true]],\n        });\n        this.dropdown.close();\n    },\n    getFailureNotificationName(failure) {\n        if (failure.type === \"snail\") {\n            return _t(\"Snailmail Failure: %(modelName)s\", { modelName: failure.modelName });\n        }\n        return super.getFailureNotificationName(...arguments);\n    },\n});\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport const FILTER_DATE_OPTION = {\n    quarter: [\"first_quarter\", \"second_quarter\", \"third_quarter\", \"fourth_quarter\"],\n};\n\n// TODO Remove this mapping, We should only need number > description to avoid multiple conversions\n// This would require a migration though\nexport const monthsOptions = [\n    { id: \"january\", description: _t(\"January\") },\n    { id: \"february\", description: _t(\"February\") },\n    { id: \"march\", description: _t(\"March\") },\n    { id: \"april\", description: _t(\"April\") },\n    { id: \"may\", description: _t(\"May\") },\n    { id: \"june\", description: _t(\"June\") },\n    { id: \"july\", description: _t(\"July\") },\n    { id: \"august\", description: _t(\"August\") },\n    { id: \"september\", description: _t(\"September\") },\n    { id: \"october\", description: _t(\"October\") },\n    { id: \"november\", description: _t(\"November\") },\n    { id: \"december\", description: _t(\"December\") },\n];\n", "/** @odoo-module */\n\nimport { loadBundle } from \"@web/core/assets\";\n\n/**\n * Load external libraries required for o-spreadsheet\n * @returns {Promise<void>}\n */\nexport async function loadSpreadsheetDependencies() {\n    await loadBundle(\"web.chartjs_lib\");\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { loadBundle } from \"@web/core/assets\";\nimport { loadSpreadsheetDependencies } from \"./helpers\";\n\nconst actionRegistry = registry.category(\"actions\");\n\n/**\n * Add a new function client action which loads the spreadsheet bundle, then\n * launch the actual action.\n * The action should be redefine in the bundle with `{ force: true }`\n * and the actual action component or function\n * @param {string} actionName\n * @param {string} [path]\n * @param {string} [displayName]\n */\nexport function addSpreadsheetActionLazyLoader(actionName, path, displayName) {\n    const actionLazyLoader = async (env, action) => {\n        // load the bundle which should redefine the action in the registry\n        await loadSpreadsheetDependencies();\n        await loadBundle(\"spreadsheet.o_spreadsheet\");\n\n        if (actionRegistry.get(actionName) === actionLazyLoader) {\n            // At this point, the real spreadsheet client action should be loaded and have\n            // replaced this function in the action registry. If it's not the case,\n            // it probably means that there was a crash in the bundle (e.g. syntax\n            // error). In this case, this action will remain in the registry, which\n            // will lead to an infinite loop. To prevent that, we push another action\n            // in the registry.\n            actionRegistry.add(\n                actionName,\n                () => {\n                    const msg = _t(\"%s couldn't be loaded\", actionName);\n                    env.services.notification.add(msg, { type: \"danger\" });\n                },\n                { force: true }\n            );\n        }\n        // then do the action again, with the actual definition registered\n        return action;\n    };\n    if (path) {\n        actionLazyLoader.path = path;\n    }\n    if (displayName) {\n        actionLazyLoader.displayName = displayName;\n    }\n    actionRegistry.add(actionName, actionLazyLoader);\n}\n\naddSpreadsheetActionLazyLoader(\"action_download_spreadsheet\");\n", "import { registry } from \"@web/core/registry\";\nimport { BinaryField, binaryField } from \"@web/views/fields/binary/binary_field\";\n\nexport class SpreadsheetBinaryField extends BinaryField {\n    static template = \"spreadsheet.SpreadsheetBinaryField\";\n\n    setup() {\n        super.setup();\n    }\n\n    async onFileDownload() {}\n}\n\nexport const spreadsheetBinaryField = {\n    ...binaryField,\n    component: SpreadsheetBinaryField,\n};\n\nregistry.category(\"fields\").add(\"binary_spreadsheet\", spreadsheetBinaryField);\n", "import { addSpreadsheetActionLazyLoader } from \"@spreadsheet/assets_backend/spreadsheet_action_loader\";\r\n\r\naddSpreadsheetActionLazyLoader(\"action_spreadsheet_dashboard\");\r\n", "import { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { Many2OneField, many2OneField } from \"@web/views/fields/many2one/many2one_field\";\nimport { Many2XAutocomplete } from \"@web/views/fields/relational_utils\";\n\nclass Many2XSpreadsheetAutocomplete extends Many2XAutocomplete {\n    setup() {\n        super.setup();\n        const actionService = useService(\"action\");\n        // Overwrite the \"Create and edit\" function to create a new spreadsheet\n        // and open it.\n        // The standard behavior opens a dialog with the form view\n        this.openMany2X = async ({ context }) => {\n            const action = await this.orm.call(\n                this.props.resModel,\n                \"action_open_new_spreadsheet\",\n                [],\n                { context }\n            );\n            this.props.update([{ id: action.params.spreadsheet_id }]);\n            await this.env.model.root.save();\n            await actionService.doAction(action);\n        };\n    }\n}\n\nclass Many2OneSpreadsheetField extends Many2OneField {\n    static components = {\n        ...Many2OneField.components,\n        // replace the Many2XAutocomplete component by our custom autocomplete\n        Many2XAutocomplete: Many2XSpreadsheetAutocomplete,\n    };\n}\n\nregistry.category(\"fields\").add(\"many2one_spreadsheet\", {\n    ...many2OneField,\n    component: Many2OneSpreadsheetField,\n});\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Notebook } from \"@web/core/notebook/notebook\";\n\nimport { Component, onWillStart, useState } from \"@odoo/owl\";\nimport { SpreadsheetSelectorPanel } from \"./spreadsheet_selector_panel\";\n\nconst LABELS = {\n    PIVOT: _t(\"pivot\"),\n    LIST: _t(\"list\"),\n    LINK: _t(\"link\"),\n    GRAPH: _t(\"graph\"),\n};\n\n/**\n * @typedef State\n * @property {Object} spreadsheets\n * @property {string} panel\n * @property {string} name\n * @property {number|null} selectedSpreadsheetId\n * @property {string} [threshold]\n * @property {Object} pagerProps\n * @property {number} pagerProps.offset\n * @property {number} pagerProps.limit\n * @property {number} pagerProps.total\n */\n\nexport class SpreadsheetSelectorDialog extends Component {\n    static template = \"spreadsheet_edition.SpreadsheetSelectorDialog\";\n    static components = { Dialog, Notebook };\n    static props = {\n        actionOptions: Object,\n        type: String,\n        threshold: { type: Number, optional: true },\n        maxThreshold: { type: Number, optional: true },\n        name: String,\n        close: Function,\n    };\n\n    setup() {\n        /** @type {State} */\n        this.state = useState({\n            threshold: this.props.threshold,\n            name: this.props.name,\n            confirmationIsPending: false,\n        });\n        this.actionState = {\n            getOpenSpreadsheetAction: () => {},\n            notificationMessage: \"\",\n        };\n        this.notification = useService(\"notification\");\n        this.actionService = useService(\"action\");\n        const orm = useService(\"orm\");\n        onWillStart(async () => {\n            const spreadsheetModels = await orm.call(\n                \"spreadsheet.mixin\",\n                \"get_selector_spreadsheet_models\"\n            );\n            this.noteBookPages = spreadsheetModels.map(({ model, display_name, allow_create }) => {\n                return {\n                    Component: SpreadsheetSelectorPanel,\n                    id: model,\n                    title: display_name,\n                    props: {\n                        model,\n                        displayBlank: allow_create,\n                        onSpreadsheetSelected: this.onSpreadsheetSelected.bind(this),\n                        onSpreadsheetDblClicked: this._confirm.bind(this),\n                    },\n                };\n            });\n        });\n    }\n\n    get nameLabel() {\n        return _t(\"Name of the %s:\", LABELS[this.props.type]);\n    }\n\n    get title() {\n        return _t(\"Select a spreadsheet to insert your %s.\", LABELS[this.props.type]);\n    }\n\n    /**\n     * @param {number|null} id\n     */\n    onSpreadsheetSelected({ getOpenSpreadsheetAction, notificationMessage }) {\n        this.actionState = {\n            getOpenSpreadsheetAction,\n            notificationMessage,\n        };\n    }\n\n    async _confirm() {\n        if (this.state.confirmationIsPending) {\n            return;\n        }\n        this.state.confirmationIsPending = true;\n        const action = await this.actionState.getOpenSpreadsheetAction();\n        const threshold = this.state.threshold ? parseInt(this.state.threshold, 10) : 0;\n        const name = this.state.name.toString();\n\n        if (this.actionState.notificationMessage) {\n            this.notification.add(this.actionState.notificationMessage, { type: \"info\" });\n        }\n        // the action can be preceded by a notification\n        const actionOpen = action.tag === \"display_notification\" ? action.params.next : action;\n        actionOpen.params = this._addToPreprocessingAction(actionOpen.params, threshold, name);\n        this.actionService.doAction(action);\n        this.props.close();\n    }\n\n    _addToPreprocessingAction(actionParams, threshold, name) {\n        return {\n            ...this.props.actionOptions,\n            preProcessingAsyncActionData: {\n                ...this.props.actionOptions.preProcessingAsyncActionData,\n                threshold,\n                name,\n            },\n            preProcessingActionData: {\n                ...this.props.actionOptions.preProcessingActionData,\n                threshold,\n                name,\n            },\n            ...actionParams,\n        };\n    }\n\n    _cancel() {\n        this.props.close();\n    }\n}\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Pager } from \"@web/core/pager/pager\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\nimport { SpreadsheetSelectorGrid } from \"../spreadsheet_selector_grid/spreadsheet_selector_grid\";\n\nimport { Component, onWillStart, useState, onWillUnmount } from \"@odoo/owl\";\n\nconst DEFAULT_LIMIT = 9;\n\n/**\n * @typedef State\n * @property {Object} spreadsheets\n * @property {string} panel\n * @property {string} name\n * @property {number|null} selectedSpreadsheetId\n * @property {string} [threshold]\n * @property {Object} pagerProps\n * @property {number} pagerProps.offset\n * @property {number} pagerProps.limit\n * @property {number} pagerProps.total\n */\n\nexport class SpreadsheetSelectorPanel extends Component {\n    static template = \"spreadsheet_edition.SpreadsheetSelectorPanel\";\n    static components = { Pager, SpreadsheetSelectorGrid };\n    static defaultProps = {\n        displayBlank: true,\n    };\n    static props = {\n        onSpreadsheetSelected: Function,\n        onSpreadsheetDblClicked: Function,\n        model: String,\n        displayBlank: {\n            type: Boolean,\n            optional: true,\n        },\n    };\n\n    setup() {\n        /** @type {State} */\n        this.state = useState({\n            spreadsheets: {},\n            selectedSpreadsheetId: null,\n            pagerProps: {\n                offset: 0,\n                limit: this.props.displayBlank ? DEFAULT_LIMIT : DEFAULT_LIMIT + 1,\n                total: 0,\n            },\n        });\n        this.keepLast = new KeepLast();\n        this.orm = useService(\"orm\");\n        this.domain = [];\n        this.debounce = undefined;\n\n        onWillStart(async () => {\n            await this._fetchSpreadsheets();\n            const selectedItem =\n                !this.props.displayBlank && this.state.spreadsheets.length\n                    ? this.state.spreadsheets[0].id\n                    : null;\n            this._selectItem(selectedItem);\n        });\n\n        onWillUnmount(() => {\n            browser.clearTimeout(this.debounce);\n        });\n\n        useHotkey(\"Enter\", () => {\n            this.props.onSpreadsheetDblClicked();\n        });\n    }\n\n    async _fetchSpreadsheets() {\n        const { offset, limit } = this.state.pagerProps;\n        const { records, total } = await this.keepLast.add(\n            this.orm.call(this.props.model, \"get_spreadsheets\", [this.domain], {\n                offset,\n                limit,\n            })\n        );\n        this.state.spreadsheets = records;\n        this.state.pagerProps.total = total;\n    }\n\n    async _getOpenSpreadsheetAction() {\n        return this.orm.call(this.props.model, \"action_open_spreadsheet\", [\n            [this.state.selectedSpreadsheetId],\n        ]);\n    }\n\n    async _getCreateAndOpenSpreadsheetAction() {\n        return this.orm.call(this.props.model, \"action_open_new_spreadsheet\");\n    }\n\n    async onSearchInput(ev) {\n        const currentSearch = ev.target.value;\n        this.domain = currentSearch !== \"\" ? [[\"name\", \"ilike\", currentSearch]] : [];\n\n        // Reset pager offset and get the total count based on the search criteria\n        this.state.pagerProps.offset = 0;\n        this._debouncedFetchSpreadsheets();\n    }\n\n    _debouncedFetchSpreadsheets() {\n        browser.clearTimeout(this.debounce);\n        this.debounce = browser.setTimeout(this._fetchSpreadsheets.bind(this), 400);\n    }\n\n    /**\n     * @param {Object} param0\n     * @param {number} param0.offset\n     * @param {number} param0.limit\n     */\n    onUpdatePager({ offset, limit }) {\n        this.state.pagerProps.offset = offset;\n        this.state.pagerProps.limit = limit;\n        this._fetchSpreadsheets();\n    }\n\n    /**\n     * @param {number|null} id\n     */\n    _selectItem(id) {\n        this.state.selectedSpreadsheetId = id;\n        const spreadsheet =\n            this.state.selectedSpreadsheetId &&\n            this.state.spreadsheets.find((s) => s.id === this.state.selectedSpreadsheetId);\n        const notificationMessage = spreadsheet\n            ? _t(\"New sheet inserted in '%s'\", spreadsheet.display_name)\n            : \"\";\n        this.props.onSpreadsheetSelected({\n            spreadsheet,\n            notificationMessage,\n            getOpenSpreadsheetAction: spreadsheet\n                ? this._getOpenSpreadsheetAction.bind(this)\n                : this._getCreateAndOpenSpreadsheetAction.bind(this),\n        });\n    }\n\n    /**\n     * @param {Object} spreadsheet\n     * @returns {string} - URL for the spreadsheet thumbnail\n     */\n    getThumbnailURL(spreadsheet) {\n        if (!spreadsheet.thumbnail) {\n            return false;\n        }\n\n        return `data:image/jpeg;charset=utf-8;base64,${spreadsheet.thumbnail}`;\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useHotkey } from \"@web/core/hotkeys/hotkey_hook\";\n\nconst DEFAULT_LIMIT = 9;\nconst BLANK_SPREADSHEET_TEMPLATE = {\n    id: null,\n    display_name: _t(\"Blank spreadsheet\"),\n    thumbnail: \"/spreadsheet/static/img/spreadsheet.svg\",\n};\n\nexport class SpreadsheetSelectorGrid extends Component {\n    static template = \"documents_spreadsheet.SpreadsheetSelectorGrid\";\n    static defaultProps = {\n        displayBlank: true,\n    };\n\n    static props = {\n        spreadsheets: Array,\n        onSpreadsheetSelected: Function,\n        onSpreadsheetDblClicked: Function,\n        getThumbnailURL: Function,\n        selectedSpreadsheetId: Number | null,\n        displayBlank: {\n            type: Boolean,\n            optional: true,\n        },\n    };\n\n    blankThumbnailPlaceholder = \"/spreadsheet/static/img/spreadsheet.svg\";\n\n    setup() {\n        useHotkey(\"ArrowRight\", () => this._onArrowKey(\"right\"), {\n            allowRepeat: true,\n        });\n        useHotkey(\"ArrowLeft\", () => this._onArrowKey(\"left\"), {\n            allowRepeat: true,\n        });\n    }\n\n    /**\n     * @returns {Array} - The list of spreadsheets to display in the grid.\n     */\n    get spreadsheets() {\n        if (!this.props.displayBlank) {\n            return this.props.spreadsheets;\n        }\n        return [BLANK_SPREADSHEET_TEMPLATE, ...this.props.spreadsheets];\n    }\n\n    /**\n     * @returns {Number} - The number of spreadsheets to display per page.\n     */\n    get itemsPerPage() {\n        return this.props.displayBlank ? DEFAULT_LIMIT : DEFAULT_LIMIT + 1;\n    }\n\n    /**\n     * Handles the arrow key press event to navigate through spreadsheets and pages.\n     * @param {left|right} direction - The direction of the arrow key.\n     */\n    async _onArrowKey(direction) {\n        const index = this.spreadsheets.findIndex(\n            (spreadsheet) => spreadsheet.id === this.props.selectedSpreadsheetId\n        );\n\n        // Navigate to the next or previous spreadsheet\n        const navigateToSpreadsheet = (newSpreadsheetId) => {\n            if (newSpreadsheetId === this.props.selectedSpreadsheetId) {\n                return;\n            }\n            this.props.onSpreadsheetSelected(newSpreadsheetId);\n        };\n\n        switch (direction) {\n            case \"left\":\n                if (index > 0 && index < this.spreadsheets.length) {\n                    // Navigate to the previous spreadsheet\n                    navigateToSpreadsheet(this.spreadsheets[index - 1].id);\n                }\n                break;\n            case \"right\":\n                if (index < this.spreadsheets.length - 1) {\n                    // Navigate to the next spreadsheet\n                    navigateToSpreadsheet(this.spreadsheets[index + 1].id);\n                }\n                break;\n            default:\n                break;\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { SpreadsheetSelectorDialog } from \"@spreadsheet_edition/assets/components/spreadsheet_selector_dialog/spreadsheet_selector_dialog\";\n\nimport { Component } from \"@odoo/owl\";\n\n/**\n * Insert a link to a view in spreadsheet\n * @extends Component\n */\nexport class InsertViewSpreadsheet extends Component {\n    static props = {};\n    static template = \"spreadsheet_edition.InsertActionSpreadsheet\";\n    static components = { DropdownItem };\n\n    setup() {\n        this.notification = useService(\"notification\");\n        this.actionService = useService(\"action\");\n        this.dialogManager = useService(\"dialog\");\n    }\n\n    //-------------------------------------------------------------------------\n    // Handlers\n    //-------------------------------------------------------------------------\n\n    async linkInSpreadsheet() {\n        const actionToLink = await this.getViewDescription();\n        // do action with action link\n        const actionOptions = {\n            preProcessingAction: \"insertLink\",\n            preProcessingActionData: actionToLink,\n        };\n\n        this.dialogManager.add(SpreadsheetSelectorDialog, {\n            type: \"LINK\",\n            actionOptions,\n            name: this.env.config.getDisplayName(),\n        });\n    }\n\n    async getViewDescription() {\n        const { resModel } = this.env.searchModel;\n        const { views = [], actionId, viewType } = this.env.config;\n        const { xml_id } = actionId\n            ? await this.actionService.loadAction(actionId, this.env.searchModel.context)\n            : {};\n        const { context } = this.env.searchModel.getIrFilterValues();\n        const action = {\n            xmlId: xml_id,\n            domain: this.env.searchModel.domain,\n            context,\n            modelName: resModel,\n            // prevent navigation to other views as we have a dedicated domain/context\n            views: views.map(([, type]) => [false, type]),\n        };\n        return {\n            viewType,\n            action,\n        };\n    }\n}\n", "/** @odoo-module */\n\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\n\nimport { Component } from \"@odoo/owl\";\n\nexport class InsertListSpreadsheetMenu extends Component {\n    static props = {};\n    static template = \"spreadsheet_edition.InsertListSpreadsheetMenu\";\n    static components = { DropdownItem };\n\n    /**\n     * @private\n     */\n    _onClick() {\n        this.env.bus.trigger(\"insert-list-spreadsheet\");\n    }\n}\n", "/** @odoo-module **/\n\nimport { patch } from \"@web/core/utils/patch\";\nimport { ListController } from \"@web/views/list/list_controller\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport const patchListControllerExportSelection ={\n    getStaticActionMenuItems() {\n        const list = this.model.root;\n        const isM2MGrouped = list.groupBy.some((groupBy) => {\n            const fieldName = groupBy.split(\":\")[0];\n            return list.fields[fieldName].type === \"many2many\";\n        });\n        const menuItems = super.getStaticActionMenuItems(...arguments);\n        menuItems[\"insert\"] = {\n            isAvailable: () => !isM2MGrouped,\n            sequence: 15,\n            icon: \"oi oi-view-list\",\n            description: _t(\"Insert in spreadsheet\"),\n            callback: () => this.env.bus.trigger(\"insert-list-spreadsheet\"),\n        };\n        return menuItems;\n    },\n};\n\n\nexport const unpatchListControllerExportSelection =  patch(ListController.prototype, patchListControllerExportSelection);\n", "/** @odoo-module **/\n\nimport { patch } from \"@web/core/utils/patch\";\nimport { ListRenderer } from \"@web/views/list/list_renderer\";\nimport { user } from \"@web/core/user\";\nimport { useService, useBus } from \"@web/core/utils/hooks\";\nimport { omit } from \"@web/core/utils/objects\";\nimport { SpreadsheetSelectorDialog } from \"../components/spreadsheet_selector_dialog/spreadsheet_selector_dialog\";\nimport { HandleField } from \"@web/views/fields/handle/handle_field\";\nimport { _t } from \"@web/core/l10n/translation\";\n\npatch(ListRenderer.prototype, {\n    /**\n     * @override\n     */\n    setup() {\n        super.setup(...arguments);\n        this.dialogService = useService(\"dialog\");\n        this.actionService = useService(\"action\");\n        useBus(this.env.bus, \"insert-list-spreadsheet\", this.insertListSpreadsheet.bind(this));\n    },\n\n    async insertListSpreadsheet() {\n        const model = this.env.model.root;\n        const count = model.groups\n            ? model.groups.reduce((acc, group) => group.count + acc, 0)\n            : model.count;\n        const selection = await model.getResIds(true);\n        const threshold = selection.length > 0 ? selection.length : Math.min(count, model.limit);\n        let name = this.env.config.getDisplayName();\n        const sortBy = model.orderBy[0]?.name;\n        const groupBy = model.groupBy[0];\n        if (sortBy || groupBy) {\n            name = _t(\"%(field name)s by %(order)s\", {\n                \"field name\": name,\n                order: model.fields[sortBy]?.string ?? groupBy,\n            });\n        }\n        const { list, fields } = await this.getListForSpreadsheet(name);\n\n        // if some records are selected, we replace the domain with a \"id in [selection]\" clause\n        if (selection.length > 0) {\n            list.domain = [[\"id\", \"in\", selection]];\n        }\n        const actionOptions = {\n            preProcessingAsyncAction: \"insertList\",\n            preProcessingAsyncActionData: { list, threshold, fields },\n        };\n\n        const params = {\n            threshold,\n            type: \"LIST\",\n            name,\n            actionOptions,\n        };\n        this.dialogService.add(SpreadsheetSelectorDialog, params);\n    },\n\n    getColumnsForSpreadsheet() {\n        const fields = this.env.model.root.fields;\n        return this.columns\n            .filter(\n                (col) =>\n                    col.type === \"field\" &&\n                    col.field.component !== HandleField &&\n                    !col.relatedPropertyField &&\n                    ![\"binary\", \"json\"].includes(fields[col.name].type)\n            )\n            .map((col) => ({ name: col.name, type: fields[col.name].type }));\n    },\n\n    async getListForSpreadsheet(name) {\n        const model = this.env.model.root;\n        const { actionId } = this.env.config;\n        const { xml_id } = actionId ? await this.actionService.loadAction(actionId, this.props.list.context) : {};\n        // Remove the `group_by` instructions\n        const fieldNames = model.fieldNames;\n        const filteredOrderBy = (model.orderBy).filter(order => fieldNames.includes(order.name));\n        return {\n            list: {\n                model: model.resModel,\n                domain: this.env.searchModel.domainString,\n                orderBy: filteredOrderBy,\n                context: omit(model.context, ...Object.keys(user.context)),\n                columns: this.getColumnsForSpreadsheet(),\n                name,\n                actionXmlId: xml_id,\n            },\n            fields: model.fields,\n        };\n    },\n});\n", "import { patch } from \"@web/core/utils/patch\";\nimport { Message } from \"@mail/core/common/message\";\nimport \"@mail/core/web/message_patch\"; // dependency ordering\n\npatch(Message.prototype, {\n    /**\n     * This function overrides the original method so that when the user tries to open a the record\n     * from a starred discussion linked to a spreadsheet cell thread, it can be redirected to the corresponding\n     * spreadsheet record.\n     * @override\n     */\n    async openRecord() {\n        if (this.message.model === \"spreadsheet.cell.thread\") {\n            const action = await this.env.services.orm.call(\n                \"spreadsheet.cell.thread\",\n                \"get_spreadsheet_access_action\",\n                [this.message.thread.id]\n            );\n            this.action.doAction(action);\n            return;\n        } else {\n            super.openRecord();\n        }\n    },\n});\n", "/** @odoo-module **/\n\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { registry } from \"@web/core/registry\";\nimport { session } from \"@web/session\";\nimport { InsertViewSpreadsheet } from \"../insert_action_link_menu/insert_action_link_menu\";\nimport { InsertListSpreadsheetMenu } from \"../list_view/insert_list_spreadsheet_menu_owl\";\n\nimport { Component } from \"@odoo/owl\";\nconst cogMenuRegistry = registry.category(\"cogMenu\");\n\nexport class SpreadsheetCogMenu extends Component {\n    static template = \"spreadsheet_edition.SpreadsheetCogMenu\";\n    static components = { Dropdown, InsertViewSpreadsheet, InsertListSpreadsheetMenu };\n    static props = {};\n}\n\ncogMenuRegistry.add(\n    \"spreadsheet-cog-menu\",\n    {\n        Component: SpreadsheetCogMenu,\n        groupNumber: 30,\n        isDisplayed: ({ config, isSmall }) =>\n            !isSmall &&\n            config.actionType === \"ir.actions.act_window\" &&\n            config.viewType !== \"form\" &&\n            session.can_insert_in_spreadsheet,\n    },\n    { sequence: 1 }\n);\n", "import { addSpreadsheetActionLazyLoader } from \"@spreadsheet/assets_backend/spreadsheet_action_loader\";\n\naddSpreadsheetActionLazyLoader(\"action_open_spreadsheet_history\");\n", "import { addSpreadsheetActionLazyLoader } from \"@spreadsheet/assets_backend/spreadsheet_action_loader\";\n\naddSpreadsheetActionLazyLoader(\"action_edit_dashboard\");\n", "import { addSpreadsheetActionLazyLoader } from \"@spreadsheet/assets_backend/spreadsheet_action_loader\";\n\naddSpreadsheetActionLazyLoader(\"action_sale_order_spreadsheet\", \"sale-order-spreadsheet\");\n", "import MainComponent from \"@stock_barcode/components/main\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(MainComponent.prototype, {\n    setup() {\n        super.setup();\n    },\n\n    async onOpenProductForm() {\n        await this.env.model.save();\n        this.env.model.openProductForm();\n    }\n});\n", "import BarcodeModel from \"@stock_barcode/models/barcode_model\";\n\nimport { patch } from \"@web/core/utils/patch\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { registry } from \"@web/core/registry\";\nimport { _t } from \"@web/core/l10n/translation\";\n\n\npatch(BarcodeModel.prototype, {\n\n     /**\n     * The purpose of this extension is to allow the user to create the product for the barcode data\n     * if no product found based on barcode lookup!\n     *\n     * @override\n     */\n    async noProductToast(barcodeData) {\n        // Applicable for the group [\"base.group_system\"]\n        // Applicable for models [\"stock.picking\", \"stock.quant\"]\n        // Applicable for the picking operation [\"receipts\"]\n        const canManageBarcodelookup = await this.groups.group_user_admin;\n        if (\n          canManageBarcodelookup && this.isValidForBarcodeLookup &&\n          [\"ean8\", \"ean13\", \"upca\"].some((encoding) =>\n            this.parser.check_encoding(barcodeData.barcode, encoding)\n          )\n        ) {\n            this.trigger(\"playSound\", \"error\");\n            if (!barcodeData.error) {\n                if (this.groups.group_tracking_lot) {\n                    barcodeData.error = _t(\n                        `This product doesn't exists either scan a package\n                                available at the picking location or create new product`\n                    );\n                } else {\n                    barcodeData.error = _t(\"This product doesn't exists\");\n                }\n            }\n            return this.notification(barcodeData.error, {\n                type: \"danger\",\n                buttons: [\n                    {\n                        name: _t(\"Create New Product\"),\n                        primary: true,\n                        onClick: () => {\n                            const notifications = registry\n                                .category(\"main_components\")\n                                .get(\"NotificationContainer\").props.notifications;\n                            for (const id in notifications) {\n                                const notification = notifications[id];\n                                if (notification.onClose) {\n                                    notification.onClose();\n                                }\n                                delete notifications[id];\n                            }\n                            return this.openProductForm(barcodeData);\n                        },\n                    },\n                ],\n            });\n        }\n        return super.noProductToast(barcodeData);\n    },\n\n    async openProductForm(barcodeData=false) {\n        return await this.action.doAction(\n            \"stock_barcode_barcodelookup.stock_barcodelookup_product_product_action\",\n            {\n                additionalContext: {\n                    \"default_barcode\": barcodeData?.barcode,\n                    \"default_is_storable\": true,\n                    \"dialog_size\": \"medium\",\n                    \"skip_barcode_check\": true,\n                },\n                props: {\n                    onSave: async (record) => {\n                        this.notification(_t(\"Product created successfully\"), { type: \"success\" });\n                        barcodeData ? await this.createNewProductLine(barcodeData) : false;\n                        return this.action.doAction({ type: \"ir.actions.act_window_close\" });\n                    },\n                },\n            }\n        );\n    },\n\n    async createNewProductLine(barcodeData) {\n        const barcodes_by_model = { \"product.product\": [barcodeData.barcode] };\n        const params = { barcodes_by_model };\n        try {\n            const result = await rpc(\"/stock_barcode/get_specific_barcode_data\", params);\n            if (Object.keys(result).length === 0) {\n                const message = _t(\"No record found for the specified barcode\");\n                return this.notification(message, {\n                    title: _t(\"Inconsistent Barcode\"),\n                    type: \"danger\",\n                });\n            }\n            this.cache.setCache(result);\n\n            // modifying the barcodeData\n            const [productRecord] = result[\"product.product\"];\n            barcodeData.match = true;\n            barcodeData.quantity = 1;\n            barcodeData.product = productRecord;\n            const fieldsParams = this._convertDataToFieldsParams(barcodeData);\n            if (barcodeData.uom) {\n                fieldsParams.uom = barcodeData.uom;\n            }\n            const currentLine = await this.createNewLine({fieldsParams});\n            if (currentLine) {\n                this._selectLine(currentLine);\n            }\n            this.trigger(\"update\");\n            return true;\n        } catch (error) {\n            return this.notification(error, {\n                title: _t(\"RPC Error\"),\n                type: \"danger\",\n            });\n        }\n    }\n});\n", "import BarcodePickingModel from \"@stock_barcode/models/barcode_picking_model\";\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(BarcodePickingModel.prototype, {\n    _mustScanProductFirst(barcodeData) {\n        return super._mustScanProductFirst(barcodeData) && !this.isValidForBarcodeLookup;\n    },\n});\n", "/** @odoo-module **/\n\nimport GroupedLineComponent from '@stock_barcode/components/grouped_line';\nimport { patch } from \"@web/core/utils/patch\";\n\n\npatch(GroupedLineComponent.prototype, {\n    get sublineProps() {\n        const props = super.sublineProps;\n        if (this.env.model.resModel === \"stock.picking.batch\") {\n            // Don't display the picking on sublines if already displayed on grouped line.\n            props.hidePickingName = Boolean(this.pickingName);\n        }\n        return props;\n    },\n\n    get displayToggleBtn() {\n        if (this.env.model.resModel === \"stock.picking.batch\" && !Boolean(this.pickingName)) {\n            return true;\n        }\n        return super.displayToggleBtn;\n    },\n\n    get linesToDisplay() {\n        if (this.env.model.resModel === \"stock.picking.batch\" && !Boolean(this.pickingName)) {\n            return this.props.line.lines;\n        }\n        return super.linesToDisplay;\n    },\n});\n", "/** @odoo-module **/\n\nimport LineComponent from '@stock_barcode/components/line';\nimport { patch } from \"@web/core/utils/patch\";\n\n\npatch(LineComponent.prototype, {\n\n    get componentClasses() {\n        return [\n            super.componentClasses,\n            this.line.colorLine !== undefined ? 'o_colored_markup' : ''\n        ].join(' ');\n    },\n\n    get pickingName() {\n        if (this.env.model.resModel === \"stock.picking.batch\") {\n            if (this.line.lines?.length > 1) {\n                // Don't display picking name if line has sublines belonging to different pickings.\n                const pickingId = this.line.picking_id.id;\n                for (const line of this.line.lines) {\n                    if (line.picking_id.id != pickingId) {\n                        return false;\n                    }\n                }\n            }\n            return this.line.picking_id.name;\n        }\n        return false;\n    },\n});\n\nLineComponent.props = [...LineComponent.props, \"hidePickingName?\"];\n", "/** @odoo-module **/\n\nimport BarcodePickingBatchModel from '@stock_barcode_picking_batch/models/barcode_picking_batch_model';\nimport MainComponent from '@stock_barcode/components/main';\nimport OptionLine from '@stock_barcode_picking_batch/components/option_line';\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(MainComponent.prototype, {\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    confirm: function (ev) {\n        ev.stopPropagation();\n        this.env.model.confirmSelection();\n    },\n\n    async exit(ev) {\n        if (this.state.view === 'barcodeLines' && this.env.model.canBeProcessed &&\n            this.env.model.needPickings && !this.env.model.needPickingType && this.env.model.pickingTypes) {\n            this.env.model.record.picking_type_id = false;\n            return this.env.model.trigger('update');\n        }\n        return await super.exit(...arguments);\n    },\n\n    get isConfiguring() {\n        return this.env.model.needPickingType || this.env.model.needPickings;\n    },\n\n    get displayActionButtons() {\n        return super.displayActionButtons && !this.isConfiguring;\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    _getModel() {\n        const { resId, resModel, rpc, notification, orm, action } = this;\n        if (this.resModel === 'stock.picking.batch') {\n            return new BarcodePickingBatchModel(resModel, resId, { rpc, notification, orm, action });\n        }\n        return super._getModel(...arguments);\n    },\n});\n\nMainComponent.components.OptionLine = OptionLine;\n", "/** @odoo-module **/\n\nimport { Component } from \"@odoo/owl\";\n\nexport default class OptionLine extends Component {\n    static props = [\"line\", \"additionalClass?\", \"responsible?\"];\n    static template = \"stock_barcode_picking_batch.OptionLine\";\n    get isSelected() {\n        if (this.env.model.needPickingType) {\n            return this.env.model.selectedPickingTypeId === this.props.line.id;\n        } else if (this.env.model.needPickings) {\n            return this.env.model.selectedPickings.indexOf(this.props.line.id) !== -1;\n        }\n    }\n\n    select() {\n        this.env.model.selectOption(this.props.line.id);\n    }\n}\n", "/** @odoo-module */\n\nimport { StockBarcodeKanbanController } from '@stock_barcode/kanban/stock_barcode_kanban_controller';\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(StockBarcodeKanbanController.prototype, {\n    setup() {\n        super.setup(...arguments);\n    },\n\n    /**\n     * Add a new batch picking from barcode\n     *\n     * @private\n     * @override\n     */\n    async createRecord() {\n        if (this.props.resModel === \"stock.picking.batch\") {\n            const action = await this.model.orm.call(\n                \"stock.picking.batch\",\n                \"open_new_batch_picking\",\n                [], { context: this.props.context }\n            );\n            if (action) {\n                return this.actionService.doAction(action);\n            }\n        }\n        return super.createRecord(...arguments);\n    },\n});\n", "/** @odoo-module */\n\nimport { markup } from '@odoo/owl';\nimport { StockBarcodeKanbanRenderer } from '@stock_barcode/kanban/stock_barcode_kanban_renderer';\nimport { useService } from '@web/core/utils/hooks';\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(StockBarcodeKanbanRenderer.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.actionService = useService(\"action\");\n        this.orm = useService(\"orm\");\n        this.activeIds = this.props.list.evalContext.active_ids;\n        this.displayTransferProtip = this.displayTransferProtip || this.resModel === 'stock.picking.batch';\n    },\n\n\n    async displayPickings() {\n        if (this.resModel === \"stock.picking\") {\n            return;\n        }\n        const action = await this.orm.call(\n            \"stock.picking.type\",\n            \"get_action_picking_tree_ready_kanban\",\n            [this.activeIds],\n        );\n        return this.displayAction(action);\n    },\n\n    async displayBatches() {\n        if (this.resModel === \"stock.picking.batch\") {\n            return;\n        }\n        const action = await this.orm.call(\n            \"stock.picking.type\",\n            \"action_picking_batch_barcode_kanban\",\n            [this.activeIds],\n        );\n        return this.displayAction(action);\n    },\n\n    displayAction(action) {\n        action.help = markup(action.help);\n        return this.actionService.doAction(action, {\n            stackPosition: \"replaceCurrentAction\",\n            additionalContext: this.props.list.evalContext,\n        });\n    },\n\n    async onWillStart() {\n        await super.onWillStart();\n        const modelToSearch = this.resModel === \"stock.picking\" ? \"stock.picking.batch\" : \"stock.picking\";\n        this.otherRecordsCount = await this.orm.call(\n            \"stock.picking.type\",\n            \"get_model_records_count\",\n            [this.activeIds, modelToSearch],\n        );\n    },\n});\n", "/** @odoo-module **/\n\nimport LazyBarcodeCache from '@stock_barcode/lazy_barcode_cache';\n\nimport { patch } from \"@web/core/utils/patch\";\n\npatch(LazyBarcodeCache.prototype, {\n    /**\n     * @override\n     */\n     _constructor() {\n        super._constructor(...arguments);\n        this.barcodeFieldByModel['stock.picking.batch'] = 'name';\n    },\n});\n", "/** @odoo-module **/\n\nimport BarcodePickingModel from '@stock_barcode/models/barcode_picking_model';\nimport { _t } from \"@web/core/l10n/translation\";\nimport { user } from '@web/core/user';\n\nexport default class BarcodePickingBatchModel extends BarcodePickingModel {\n    constructor(params) {\n        super(...arguments);\n        this.formViewReference = 'stock_barcode_picking_batch.stock_barcode_batch_picking_view_info';\n        this.validateMessage = _t(\"The Batch Transfer has been validated\");\n        this.validateMethod = 'action_done';\n    }\n\n    setData(data) {\n        super.setData(...arguments);\n        this.groupingLinesEnabled = this.groupingLinesEnabled || this.config.group_lines_by_product;\n        // In case it's a new batch, we must display the pickings selector first.\n        if (this.record.state === 'draft' && this.record.picking_ids.length === 0) {\n            this.selectedPickings = [];\n            this._allowedPickings = data.data.allowed_pickings;\n            this.pickingTypes = data.data.records[\"stock.picking.type\"];\n            for (const picking of this._allowedPickings) {\n                if (picking.user_id) {\n                    picking.user_id = this.cache.getRecord('res.users', picking.user_id);\n                }\n                if (picking.batch_id) {\n                    picking.batch_id = this.cache.getRecord('stock.picking.batch', picking.batch_id);\n                }\n                if (picking.partner_id) {\n                    picking.partner_id = this.cache.getRecord('res.partner', picking.partner_id);\n                }\n            }\n            if (!this.record.picking_type_code) {\n                this.selectedPickingTypeId = false;\n            }\n        }\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    get allowedPickings() {\n        const pickingTypeId = this.record.picking_type_id.id;\n        if (!pickingTypeId || !this._allowedPickings) {\n            return [];\n        }\n        return this._allowedPickings.filter(picking => picking.picking_type_id === pickingTypeId);\n    }\n\n    askBeforeNewLinesCreation(product) {\n        return product && !this.currentState.lines.some(line => line.product_id.id === product.id);\n    }\n\n    get backordersDomain() {\n        return [[\"backorder_id\", \"in\", this.record.picking_ids]];\n    }\n\n    get barcodeInfo() {\n        const barcodeInfo = {};\n        if ((this.needPickings || this.needPickingType) && !this._allowedPickings.length) {\n            // Special case: the batch need to be configured but there is no pickings available.\n            barcodeInfo.class = \"picking_batch_not_possible\";\n            barcodeInfo.message = _t(\"No ready transfers found\");\n            barcodeInfo.warning = true;\n        } else if (this.needPickingType) {\n            barcodeInfo.class = \"picking_batch_select_type\";\n            barcodeInfo.message = _t(\"Select an operation type for batch transfer\");\n        } else if (this.needPickings) {\n            barcodeInfo.class = \"picking_batch_select_transfers\";\n            barcodeInfo.message = _t(\"Select transfers for batch transfer\");\n        } else if (this.isDone) {\n            barcodeInfo.class = \"picking_already_done\";\n            barcodeInfo.message = _t(\"This batch transfer is already done\");\n            barcodeInfo.warning = true;\n        } else if (this.isCancelled) {\n            barcodeInfo.class = \"picking_already_cancelled\";\n            barcodeInfo.message = _t(\"This batch transfer is cancelled\");\n            barcodeInfo.warning = true;\n        } else if (this.record.state === 'draft') {\n            barcodeInfo.class = \"picking_batch_draft\";\n            barcodeInfo.message =  _t(\"This batch transfer is still draft, it must be confirmed before being processed\");\n            barcodeInfo.warning = true;\n        }\n        if (barcodeInfo.message) {\n            barcodeInfo.icon = barcodeInfo.warning ? \"exclamation-triangle\" : \"hand-pointer-o\";\n            barcodeInfo.warning = true;\n        }\n        return barcodeInfo.message ? barcodeInfo : super.barcodeInfo;\n    }\n\n    get canBeProcessed() {\n        if (this.record.state === 'draft') {\n            return this.needPickingType || this.needPickings;\n        }\n        return super.canBeProcessed;\n    }\n\n    get cancelLabel() {\n        return _t(\"Cancel Batch Transfer\");\n    }\n\n    get canConfirmSelection() {\n        if (this.needPickingType) {\n            return Boolean(this.selectedPickingTypeId);\n        } else if (this.needPickings) {\n            return Boolean(this.selectedPickings.length);\n        }\n    }\n\n    /**\n     * Depending of the batch's state, will confirm the picking type selection or the pickings\n     * selection. In the latter, it will confirm the batch transfer and reload its data.\n     */\n    async confirmSelection() {\n        if (this.needPickingType && this.selectedPickingTypeId) {\n            // Applies the selected picking type to the batch.\n            this.record.picking_type_id = this.cache.getRecord(\"stock.picking.type\", this.selectedPickingTypeId);\n            this.trigger('update');\n        } else if (this.needPickings && this.selectedPickings.length) {\n            // Adds the selected pickings to the batch.\n            const data = await this.orm.call(\n                'stock.picking.batch',\n                'action_add_pickings_and_confirm',\n                [[this.resId],\n                {\n                    picking_type_id: this.record.picking_type_id.id,\n                    picking_ids: this.selectedPickings,\n                    state: 'in_progress',\n                }]\n            );\n            await this.refreshCache(data.records);\n            this.selectedPickings = [];\n            this.config = data.config || {}; // Get the picking type's scan restrictions configuration.\n            this.trigger('update');\n        }\n    }\n\n    get canCreateNewLot() {\n        return this.picking.use_create_lots;\n    }\n\n    displayLineQtyDemand(line) {\n        if (this.config.group_lines_by_product && this.getQtyDemand(line) && line.batchParentLine) {\n            return true;\n        }\n        return super.displayLineQtyDemand(line);\n    }\n\n    get displaySignatureButton() {\n        return false;\n    }\n\n    groupLines() {\n        const groupedLines = super.groupLines();\n        if (this.config.group_lines_by_product) {\n            // Make a second grouping by product.\n            const lines = [...groupedLines];\n            const groupedLinesByKey = {};\n            for (let index = lines.length - 1; index >= 0; index--) {\n                const line = lines[index];\n                const key = super.groupKey(line);\n                if (!groupedLinesByKey[key]) {\n                    groupedLinesByKey[key] = [];\n                }\n                if (line.batchParentLine) {\n                    // Remove previous parent line's link.\n                    delete line.batchParentLine;\n                }\n                const linesToPush = line.lines ? line.lines : [line];\n                lines.splice(index, 1);\n                groupedLinesByKey[key].push(...linesToPush);\n            }\n            for (const sublines of Object.values(groupedLinesByKey)) {\n                if (sublines.length === 1) {\n                    lines.push(...sublines);\n                    continue;\n                }\n                const ids = [];\n                const virtual_ids = [];\n                let [qtyDemand, qtyDone] = [0, 0];\n                for (const subline of sublines) {\n                    ids.push(subline.id);\n                    virtual_ids.push(subline.virtual_id);\n                    qtyDemand += this.getQtyDemand(subline);\n                    qtyDone += this.getQtyDone(subline);\n                }\n                const groupedLine = this._groupBatchSublines(sublines, ids, virtual_ids, qtyDemand, qtyDone);\n                lines.push(groupedLine);\n            }\n            this._groupedLines = this._sortLine(lines);\n            return this._groupedLines;\n        }\n        return groupedLines;\n    }\n\n    _groupBatchSublines(sublines, ids, virtual_ids, qtyDemand, qtyDone) {\n        const sortedSublines = this._sortLine(sublines);\n        // Use the line with lowest ID as the reference (info shown on summary\n        // line and also the move line opened for the form view.)\n        const referenceLine = sortedSublines.reduce((result, line) => {\n            return line.id && (!result.id || (result.id > line.id)) ? line : result;\n        })\n        const groupedLine = Object.assign({}, referenceLine, {\n            ids,\n            lines: sortedSublines,\n            opened: false,\n            virtual_ids,\n            reserved_uom_qty: qtyDemand,\n            qty_done: qtyDone,\n        });\n        for (const subline of sublines) {\n            subline.batchParentLine = groupedLine;\n        }\n        return groupedLine;\n    }\n\n    groupKey(line) {\n        return `${line.picking_id.id}_` + super.groupKey(...arguments);\n    }\n\n    lineCannotBeGrouped(line) {\n        if (this.config.group_lines_by_product) {\n            return Boolean(line.lines);\n        }\n        return super.lineCannotBeGrouped(...arguments);\n    }\n\n    get needPickings() {\n        return this.record.state === 'draft' && this.record.picking_ids.length === 0;\n    }\n\n    get needPickingType() {\n        return this.record.state === 'draft' && !this.record.picking_type_id;\n    }\n\n    get printButtons() {\n        return [{\n            name: _t(\"Print Batch Transfer\"),\n            class: 'o_print_picking_batch',\n            method: 'action_print',\n        }, {\n            name: _t(\"Print Product Labels\"),\n            class: 'o_print_picking_batch_labels',\n            method: 'action_open_label_layout',\n        }];\n    }\n\n    get reloadingMoveLines() {\n        return super.reloadingMoveLines && !this.selectedPickings?.length;\n    }\n\n    selectOption(id) {\n        if (this.needPickingType) { // Selects a picking type.\n            this.selectedPickingTypeId = this.selectedPickingTypeId === id ? false : id;\n            this.trigger('update');\n        } else if (this.needPickings) { // Selects a picking.\n            if (this.selectedPickings.indexOf(id) !== -1) {\n                // If picking already selected, removes it from the selected ones.\n                this.selectedPickings.splice(this.selectedPickings.indexOf(id), 1);\n            } else {\n                this.selectedPickings.push(id);\n            }\n            this.trigger('update');\n        }\n    }\n\n    get shouldOpenSignatureModal() {\n        return false;\n    }\n\n    get useExistingLots() {\n        return this.picking.use_existing_lots;\n    }\n\n    // -------------------------------------------------------------------------\n    // Private\n    // -------------------------------------------------------------------------\n\n    _getLinesToMove() {\n        const configScanProd = this.config.restrict_scan_product;\n        const configScanDest = this.config.restrict_scan_dest_location;\n        const lines = super._getLinesToMove();\n        // We may have multiple pickings in a batch which move the same product,\n        // then we should just update them all together.\n        if (configScanDest === 'mandatory' && configScanProd) {\n            lines.push(...this.previousScannedLines);\n        }\n        return Array.from(new Set(lines));\n    }\n\n    async _assignEmptyPackage(line, resultPackage) {\n        await super._assignEmptyPackage(...arguments);\n        this._suggestPackages();\n    }\n\n    _cancelNotification() {\n        this.notification(_t(\"The batch picking has been cancelled\"));\n    }\n\n    _canOverrideTrackingNumber(line, newLotName) {\n        return super._canOverrideTrackingNumber(...arguments) || this.getQtyDone(line) === 0;\n    }\n\n    _createLinesState() {\n        const lines = super._createLinesState();\n        const pickings = this.record.picking_ids;\n        this.colorByPickingId = new Map(pickings.map((p, i) => [p, i * (360 / pickings.length)]));\n\n        for (const line of lines) {\n            if (!this.config.group_lines_by_product) {\n                // Don't color lines if they are grouped by product/locations (lines from different\n                // pickings can be grouped together so it makes no sense.)\n                line.colorLine = this.colorByPickingId.get(line.picking_id);\n            }\n            line.picking_id = line.picking_id && this.cache.getRecord('stock.picking', line.picking_id);\n        }\n        return lines;\n    }\n\n    _createState() {\n        super._createState(...arguments);\n        this._suggestPackages();\n        if (this.record.picking_ids.length < 1) {\n            return new Error(\"No picking related\");\n        }\n        // Get the first picking as a reference for some fields the batch hasn't.\n        this.picking = this.cache.getRecord('stock.picking', this.record.picking_ids[0]);\n    }\n\n    _defaultLocation() {\n        return this.picking && this.cache.getRecord('stock.location', this.picking.location_id);\n    }\n\n    _defaultDestLocation() {\n        return this.picking && this.cache.getRecord('stock.location', this.picking.location_dest_id);\n    }\n\n    _findLine(barcodeData) {\n        // With batch pickings, we can have multiple grouped lines for the same tracked product if\n        // different pickings use the same tracked product. This override ensures once the user\n        // started to scan lot/serial numbers for a grouped line, we complete it before looking for\n        // another grouped line, even if the scanned LN/SN is reserved in another picking.\n        const {lot, lotName, product} = barcodeData;\n        const dataLotName = lotName || (lot && lot.name) || false;\n        if (this.selectedLine && this.selectedLine.product_id.id === product.id && dataLotName) {\n            const parentLine = this._getParentLine(this.selectedLine);\n            if (parentLine && this._lineIsNotComplete(parentLine)) {\n                let foundLine = false;\n                for (const line of parentLine.lines) {\n                    const lineLotName = line.lot_name || (line.lot_id && line.lot_id.name) || false;\n                    const sameLotName = Boolean(lineLotName && dataLotName === lineLotName);\n                    if (dataLotName && (sameLotName || this._canOverrideTrackingNumber(line, dataLotName))) {\n                        foundLine = line;\n                        if (sameLotName) {\n                            // Prioritize this line if it has the scanned lot.\n                            break;\n                        }\n                    }\n                }\n                return foundLine;\n            }\n        }\n        return super._findLine(...arguments);\n    }\n\n    _getNewLineDefaultValues(fieldsParams) {\n        // Adds the default picking id and its corresponding color on the line.\n        const defaultValues = super._getNewLineDefaultValues(...arguments);\n        let line = this.selectedLine;\n        if (!line) {\n            if (this.lastScanned.packageId) {\n                const lines = this._moveEntirePackage() ? this.packageLines : this.pageLines;\n                line = lines.find(l => l.package_id && l.package_id.id === this.lastScanned.packageId);\n            } else if (this.pageLines.length) {\n                line = this.pageLines[0];\n            }\n        }\n        // Get the line's picking as the default one, or take the batch's first one.\n        const defaultPicking = line?.picking_id || this.picking;\n        if (!this.config.group_lines_by_product) {\n            // Don't add the color if lines are grouped by product.\n            defaultValues.colorLine = this.colorByPickingId.get(defaultPicking.id);\n        }\n        defaultValues.picking_id = defaultPicking;\n        return defaultValues;\n    }\n\n    _getNewLineDefaultContext() {\n        const defaultContextValues = super._getNewLineDefaultContext(...arguments);\n        defaultContextValues.default_batch_id = this.record.id;\n        defaultContextValues.default_picking_id = this.record.picking_ids[0];\n        return defaultContextValues;\n    }\n\n    _getScanPackageMessage(line) {\n        if (line?.suggested_package) {\n            return _t(\"Scan the package %s\", line.suggested_package);\n        }\n        return super._getScanPackageMessage(...arguments);\n    }\n\n    _incrementTrackedLine() {\n        return !(this.picking.use_create_lots || this.picking.use_existing_lots);\n    }\n\n    _lineCannotBeTaken(line) {\n        // Don't take another line if the selected one is not complete and are from different pickings.\n        let selectedLine = this.selectedLine;\n        if (selectedLine && selectedLine.product_id?.tracking !== \"none\") {\n            selectedLine = this._getParentLine(selectedLine) || selectedLine;\n        }\n        return (\n            selectedLine &&\n            line.product_id.id === selectedLine.product_id.id &&\n            selectedLine.qty_done < selectedLine.reserved_uom_qty &&\n            line.picking_id.id != selectedLine.picking_id.id\n        );\n    }\n\n    _moveEntirePackage() {\n        return this.picking && this.picking.picking_type_entire_packs;\n    }\n\n    _sortingMethod(l1, l2) {\n        const res = super._sortingMethod(...arguments);\n        if (res) {\n            return res;\n        }\n        // Sort by picking's name.\n        const picking1 = l1.picking_id && l1.picking_id.name || '';\n        const picking2 = l2.picking_id && l2.picking_id.name || '';\n        if (picking1 < picking2) {\n            return -1;\n        } else if (picking1 > picking2) {\n            return 1;\n        }\n        return 0;\n    }\n\n    _suggestPackages() {\n        const suggestedPackagesByPicking = {};\n        // Checks if a line has a result package, and if so, links it to the according picking.\n        for (const line of this.currentState.lines) {\n            if (line.result_package_id && !suggestedPackagesByPicking[line.picking_id.id]) {\n                suggestedPackagesByPicking[line.picking_id.id] = line.result_package_id.name;\n            }\n        }\n        // Suggests a package to scan for each picking's line if its picking is linked to a package.\n        for (const line of this.currentState.lines) {\n            if (!line.result_package_id && suggestedPackagesByPicking[line.picking_id.id]) {\n                line.suggested_package = suggestedPackagesByPicking[line.picking_id.id];\n            }\n        }\n    }\n\n    /**\n     * Set the batch's responsible if the batch or one of its picking is unassigned.\n     */\n    async _setUser() {\n        if (this._shouldAssignUser()) {\n            this.record.user_id = user.userId;\n            const pickings = [];\n            for (const pickingId of this.record.picking_ids) {\n                const picking = this.cache.getRecord('stock.picking', pickingId);\n                picking.user_id = user.userId;\n                pickings.push(picking);\n            }\n            this.cache.setCache({'stock.picking': pickings});\n            await this.orm.write(this.resModel, [this.record.id], { user_id: user.userId });\n        }\n    }\n\n    _shouldAssignUser() {\n        // First checks if user should be assigned to batch...\n        if (this.record.user_id != user.userId)\n            return true;\n        // ... then checks if user should be assigned to atleast one picking.\n        for (const pickingId of this.record.picking_ids) {\n            const picking = this.cache.getRecord('stock.picking', pickingId);\n            if (picking.user_id != user.userId)\n                return true;\n        }\n        return false;\n    }\n\n}\n", "import { onWillStart } from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { ROUTES, WelcomeScreen } from \"@website/client_actions/configurator/configurator\";\n\nexport const WEBSITE_GENERATOR_ROUTE = 6;\n\npatch(WelcomeScreen.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.orm = useService(\"orm\");\n        this.ui = useService(\"ui\");\n\n        onWillStart(async () => {\n            this.showWebsiteGeneratorButton = await this.orm.call(\n                \"website\",\n                \"is_website_generator_available\",\n            );\n        });\n    },\n\n    async goToWebsiteGeneratorRequest() {\n        if (ROUTES.websiteGenerator) {\n            this.props.navigate(ROUTES.websiteGenerator);\n            return;\n        }\n\n        // install website_generator\n        this.ui.block();\n        const modules = await this.orm.searchRead(\n            \"ir.module.module\",\n            [[\"name\", \"=\", \"website_generator\"]],\n            [\"id\"],\n        );\n        await this.orm.call(\"ir.module.module\", \"button_immediate_install\", [[modules[0].id]]);\n        this.props.navigate(WEBSITE_GENERATOR_ROUTE, true);\n        this.ui.unblock();\n    },\n});\n", "/** @odoo-module **/\nimport { patch } from \"@web/core/utils/patch\";\nimport { WebsitePreview } from '@website/client_actions/website_preview/website_preview';\n\npatch(WebsitePreview.prototype, {\n    /**\n     * @override\n     */\n    _isTopWindowURL({ pathname }) {\n        return (\n            pathname && (\n                pathname.startsWith('/knowledge/article/')\n                || pathname.includes('/knowledge/home')\n            )\n        ) || super._isTopWindowURL(...arguments);\n    }\n});\n", "/** @odoo-module **/\nimport { patch } from \"@web/core/utils/patch\";\n\nimport { PermissionPanel } from \"@knowledge/components/permission_panel/permission_panel\";\nimport { CopyClipboardCharField } from \"@web/views/fields/copy_clipboard/copy_clipboard_field\";\n\nconst PermissionPanelWebsiteKnowledgePatch = {\n    toggleWebsitePublished() {\n        if (\n            (this.props.record.data.user_can_write && this.isInternalUser) ||\n            this.state.user_is_admin\n        ) {\n            this.props.record.update(\n                { website_published: !this.props.record.data.website_published },\n                { save: true }\n            );\n        }\n    }\n};\n\npatch(PermissionPanel.prototype, PermissionPanelWebsiteKnowledgePatch);\nPermissionPanel.components = {\n    ...PermissionPanel.components,\n    CopyClipboardCharField,\n};\n", "import { loadBundle } from \"@web/core/assets\";\nimport { registry } from \"@web/core/registry\";\nimport { getColor } from \"@web/core/colors/colors\";\nimport { standardFieldProps } from \"@web/views/fields/standard_field_props\";\n\nimport { Component, onWillStart, useEffect, useRef } from \"@odoo/owl\";\nimport { cookie } from \"@web/core/browser/cookie\";\n\nexport class CommissionGraphField extends Component {\n    static template = \"sale_commission.GraphField\";\n    static props = {\n        ...standardFieldProps,\n    };\n\n    setup() {\n        this.chart = null;\n        this.canvasRef = useRef(\"canvas\");\n        this.data = JSON.parse(this.props.record.data[this.props.name]);\n\n        onWillStart(async () => await loadBundle(\"web.chartjs_lib\"));\n\n        useEffect(() => {\n            this.renderChart();\n            return () => {\n                if (this.chart) {\n                    this.chart.destroy();\n                }\n            };\n        });\n    }\n\n    /**\n     * Instantiates a Chart (Chart.js lib) to render the graph according to\n     * the current config.\n     */\n    renderChart() {\n        if (this.chart) {\n            this.chart.destroy();\n        }\n        this.data = JSON.parse(this.props.record.data[this.props.name]);\n        this.chart = new Chart(this.canvasRef.el, this.getLineChartConfig());\n    }\n\n    GCD(a, b) {\n        if (a < b) return this.GCD(b, a);\n        if (b == 0) return a;\n        return this.GCD(b, a % b);\n    }\n\n    getLineChartConfig() {\n        const labels = this.data.values.map(function (pt) {\n            return pt.x;\n        });\n\n        let gcd = 0;\n        this.data.values.forEach(v => {\n            gcd = this.GCD(v.x, gcd);\n        })\n\n        const color10 = getColor(3, cookie.get(\"color_scheme\"), \"odoo\");\n        const currency = this.data.currency;\n        return {\n            type: \"line\",\n            data: {\n                labels,\n                datasets: [\n                    {\n                        color10,\n                        data: this.data.values,\n                        fill: \"start\",\n                        borderWidth: 2,\n                    },\n                ],\n            },\n            options: {\n                plugins: {\n                    legend: { display: false },\n                    tooltip: {\n                        enabled: true,\n                        intersect: false,\n                        position: \"nearest\",\n                        caretSize: 0,\n                    },\n                },\n                scales: {\n                    x: {\n                        type: 'linear',\n                        ticks: {\n                            stepSize: gcd,\n                            callback: function(value, index, ticks) {\n                                return value + '%'\n                            }\n                        }\n                    },\n                    y: {\n                        type: 'linear',\n                        ticks: {\n                            callback: function(value, index, ticks) {\n                                return value + currency\n                            }\n                        }\n                    },\n                },\n                elements: {\n                    line: {\n                        tension: 0.000001,\n                    },\n                },\n            },\n        };\n    }\n}\n\nexport const commissionGraphField = {\n    component: CommissionGraphField,\n    supportedTypes: [\"text\"],\n};\n\nregistry.category(\"fields\").add(\"commission_graph\", commissionGraphField);\n", "/** @odoo-module **/\n\nimport { startWebClient } from \"@web/start\";\nimport { WebClientEnterprise } from \"./webclient/webclient\";\n\n/**\n * This file starts the enterprise webclient. In the manifest, it replaces\n * the community main.js to load a different webclient class\n * (WebClientEnterprise instead of WebClient)\n */\nstartWebClient(WebClientEnterprise);\n", "import { mountComponent } from \"./env\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { session } from \"@web/session\";\nimport { hasTouch } from \"@web/core/browser/feature_detection\";\nimport { user } from \"@web/core/user\";\nimport { Component, whenReady } from \"@odoo/owl\";\n\n/**\n * Function to start a webclient.\n * It is used both in community and enterprise in main.js.\n * It's meant to be webclient flexible so we can have a subclass of\n * webclient in enterprise with added features.\n *\n * @param {Component} Webclient\n */\nexport async function startWebClient(Webclient) {\n    odoo.info = {\n        db: session.db,\n        server_version: session.server_version,\n        server_version_info: session.server_version_info,\n        isEnterprise: session.server_version_info.slice(-1)[0] === \"e\",\n    };\n    odoo.isReady = false;\n\n    await whenReady();\n    const app = await mountComponent(Webclient, document.body, { name: \"Odoo Web Client\" });\n    const { env } = app;\n    Component.env = env;\n\n    const classList = document.body.classList;\n    if (localization.direction === \"rtl\") {\n        classList.add(\"o_rtl\");\n    }\n    if (user.userId === 1) {\n        classList.add(\"o_is_superuser\");\n    }\n    if (env.debug) {\n        classList.add(\"o_debug\");\n    }\n    if (hasTouch()) {\n        classList.add(\"o_touch_device\");\n    }\n    // delete odoo.debug; // FIXME: some legacy code rely on this\n    odoo.isReady = true;\n}\n"], "file": "/web/assets/14e0130/web.assets_web.js", "sourceRoot": "../../../"}